20 KiB
SQL Parser Architecture Review
Current Architecture (Updated: February 2026)
✅ Architecture Status: WELL-DESIGNED
The codebase uses a namespace-based architecture with inheritance, which is clean, maintainable, and follows .NET best practices.
Architecture Pattern
Namespace Organization
The architecture uses two namespaces to separate SQL Server (T-SQL) and Snowflake implementations:
Strata.SqlTools.SqlServer- Base implementations for T-SQLStrata.SqlTools.Snowflake- Snowflake-specific implementations that inherit from SqlServer
Class Structure
All classes use the same simple names in their respective namespaces, differentiated by namespace rather than class name prefix. This is the preferred .NET pattern.
Base Classes (SqlServer Namespace)
-
QueryBreakdown(SqlServer.QueryBreakdown)- Instance-based query breakdown
- Manages query clauses and parameters
- Uses
@paramsyntax for T-SQL - Base functionality for all SQL dialects
- Key Methods:
GetClauses()- ReturnsSqlClausesobject from current propertiesApplyClauses(SqlClauses?)- Applies clauses to query (null-safe)AddWithClause()- Adds Common Table Expressions (CTEs)Parse(string sql)- Static parser for SQL strings
-
StatementParser(SqlServer.StatementParser)- Provides parsing utilities
- Methods:
NormalizeSql(),RemoveSqlComments(),ExtractSetupClauses(), etc. - Handles T-SQL specific parsing logic
- Uses
[identifier]syntax for identifiers
-
StatementExpressionParser(SqlServer.StatementExpressionParser)- Expression tree parsing for T-SQL
- Uses
StatementReadertokenizer - Converts SQL strings to expression trees
-
StatementReader(SqlServer.StatementReader)- Tokenizer/lexer for T-SQL
- Handles
[identifier]syntax - Character-by-character parsing
- Returns tokens for parser consumption
Snowflake Classes (Snowflake Namespace)
All Snowflake classes inherit from their SqlServer counterparts and override only Snowflake-specific behavior:
-
QueryBreakdown(Snowflake.QueryBreakdown) - ✅ CORRECT PATTERN- Inherits from:
SqlServer.QueryBreakdown - Snowflake-specific features:
- Adds
:paramsyntax support (in addition to@param) - Overrides
GetSql()for Snowflake formatting - Handles Snowflake-specific parameter patterns
- Adds
- Calls base class: Yes, defers to parent where appropriate
- Inherits from:
-
StatementParser(Snowflake.StatementParser) - ✅ CORRECT PATTERN- Inherits from:
SqlServer.StatementParser - Snowflake-specific features:
- Handles
QUALIFYandLIMITkeywords - Supports double-quote identifiers
"identifier" - Understands
:parametersyntax - Snowflake setup clauses (ALTER SESSION, CREATE STAGE)
- Handles
- Calls base class: Yes, reuses common parsing methods
- Inherits from:
-
StatementExpressionParser(Snowflake.StatementExpressionParser) - ✅ CORRECT PATTERN- Inherits from:
SqlServer.StatementExpressionParser - Snowflake-specific features:
- Uses Snowflake
StatementReaderinstead of SqlServer version - Handles Snowflake identifier conventions (typically uppercase)
- Supports double-quoted identifiers
- Uses Snowflake
- Calls base class: Yes, inherits core parsing logic
- Inherits from:
-
StatementReader(Snowflake.StatementReader) - ✅ CORRECT PATTERN- Inherits from:
SqlServer.StatementReader - Snowflake-specific features:
- Adds double-quote identifier support
"identifier" - Handles Snowflake naming conventions
- Adds double-quote identifier support
- Calls base class: Yes, overrides only tokenization of identifiers
- Inherits from:
Key Architectural Strengths
✅ 1. Namespace-Based Organization
Instead of using class name prefixes (e.g., SqlStatementParser, SnowflakeStatementParser), the codebase uses namespace qualification:
// Clean namespace-based approach (CURRENT)
using SqlServerParser = Strata.SqlTools.Statements.SqlServer.StatementParser;
using SnowflakeParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
var sqlServerParser = new SqlServerParser();
var snowflakeParser = new SnowflakeParser();
Benefits:
- Shorter, cleaner class names
- Clear separation of concerns via namespaces
- Follows .NET Framework/Core conventions
- Easy to add new SQL dialects (PostgreSQL, MySQL, etc.)
✅ 2. Inheritance with Selective Overrides
Snowflake classes inherit from SqlServer base classes and override only dialect-specific behavior:
public class StatementParser : SqlServer.StatementParser
{
// Inherits all common SQL parsing logic
// Only overrides Snowflake-specific methods
}
Benefits:
- DRY principle - shared logic in one place
- Bug fixes to common parsing benefit all dialects
- Clear identification of dialect-specific behavior
- Minimal code duplication
✅ 3. Proper Delegation Pattern
The Snowflake implementation properly delegates to base classes:
// Example from Snowflake.QueryBreakdown
protected override string GetParameterPattern()
{
// Snowflake supports both :param and @param
return base.GetParameterPattern() + "|:\\w+";
}
✅ 4. Clear Separation of Concerns
- SqlServer namespace: T-SQL standard implementation (most widely used SQL dialect)
- Snowflake namespace: Snowflake-specific extensions
- Classes folder: Shared data structures including:
- Clause Types:
SqlClause,SqlExpressionClause,WithClause,SqlClauses - Interfaces:
ISqlClause,ISqlExpressionClause,IWithClause - SQL Structures:
SqlTable,SqlJoin,SqlFrom,SqlFilter - Helpers:
SelectClauseColumn,QueryParam,SqlBreakdownBase,SelectSource
- Clause Types:
- Expressions folder: Expression tree components used by all dialects
- Interfaces folder: Core contracts (
IQueryBreakdown,IStatementReader,IStatementExpressionParser)
Architecture Diagrams
High-Level Package Structure
graph TB
subgraph "Strata.SqlTools"
subgraph "SqlServer Namespace (Base)"
SS_Parser[StatementParser]
SS_Reader[StatementReader]
SS_ExprParser[StatementExpressionParser]
SS_Query[QueryBreakdown]
end
subgraph "Snowflake Namespace (Dialect)"
SF_Parser[StatementParser]
SF_Reader[StatementReader]
SF_ExprParser[StatementExpressionParser]
SF_Query[QueryBreakdown]
end
subgraph "Classes (Shared)"
Clause[SqlClause, ISqlClause]
ExprClause[SqlExpressionClause, ISqlExpressionClause]
WithClause[WithClause, IWithClause]
SqlClauses[SqlClauses]
Tables[SqlTable, SqlJoin, SqlFrom]
Filters[SqlFilter]
Params[QueryParam, SelectClauseColumn]
Base[SqlBreakdownBase, SelectSource]
end
subgraph "Expressions"
Expr[Expression base]
Binary[BinaryExpression]
Column[ColumnExpression]
Literal[LiteralExpression]
Funcs[Functions: Sum, Avg, Count, etc.]
end
subgraph "Interfaces"
IQuery[IQueryBreakdown]
IReader[IStatementReader]
IParser[IStatementExpressionParser]
end
end
SF_Parser -.inherits.-> SS_Parser
SF_Reader -.inherits.-> SS_Reader
SF_ExprParser -.inherits.-> SS_ExprParser
SF_Query -.inherits.-> SS_Query
SS_Query -.implements.-> IQuery
SF_Query -.implements.-> IQuery
SS_Query -.uses.-> Clause
SS_Query -.uses.-> ExprClause
SS_Query -.uses.-> WithClause
SS_Query -.uses.-> SqlClauses
WithClause -.uses.-> IQuery
WithClause -.uses.-> SqlClauses
style SS_Parser fill:#e1f5ff
style SS_Reader fill:#e1f5ff
style SS_ExprParser fill:#e1f5ff
style SS_Query fill:#e1f5ff
style SF_Parser fill:#fff4e1
style SF_Reader fill:#fff4e1
style SF_ExprParser fill:#fff4e1
style SF_Query fill:#fff4e1
QueryBreakdown Class Hierarchy
classDiagram
class IQueryBreakdown {
<<interface>>
+ISqlExpressionClause SelectClause
+ISqlClause FromClause
+ISqlExpressionClause WhereClause
+ISqlExpressionClause GroupByClause
+ISqlExpressionClause HavingClause
+ISqlExpressionClause OrderByClause
+void AddParameter()
+void AddWhereClause()
+void MergeWith()
+string GetSql()
+SqlClauses GetClauses()
+void ApplyClauses()
}
class QueryBreakdown_SqlServer {
<<SqlServer>>
+ISqlExpressionClause SelectClause
+ISqlClause FromClause
+ISqlExpressionClause WhereClause
+List~IWithClause~ WithClauses
+Dictionary~string,object~ Parameters
+void AddWithClause()
+virtual SqlClauses GetClauses()
+virtual void ApplyClauses()
+virtual string GetSql()
+static QueryBreakdown Parse()
}
class QueryBreakdown_Snowflake {
<<Snowflake>>
+override string GetSql()
#override IStatementExpressionParser CreateExpressionParser()
}
IQueryBreakdown <|.. QueryBreakdown_SqlServer
QueryBreakdown_SqlServer <|-- QueryBreakdown_Snowflake
WITH Clause (CTE) Architecture
classDiagram
class ISqlClause {
<<interface>>
+string? Clause
+string? Comment
}
class IWithClause {
<<interface>>
+string TableName
+SqlClauses? Sql
+IQueryBreakdown? Query
}
class SqlClause {
+string? Clause
+string? Comment
}
class WithClause {
-SqlClauses? _sql
-IQueryBreakdown? _query
+string TableName
+SqlClauses? Sql
+IQueryBreakdown? Query
+WithClause()
+WithClause(tableName, query)
+WithClause(tableName, sql)
}
class SqlClauses {
+ISqlExpressionClause? SelectClause
+ISqlClause? FromClause
+ISqlExpressionClause? WhereClause
+ISqlExpressionClause? GroupByClause
+ISqlExpressionClause? HavingClause
+ISqlExpressionClause? OrderByClause
+SqlClauses Copy()
}
class IQueryBreakdown {
<<interface>>
+SqlClauses GetClauses()
+void ApplyClauses(SqlClauses?)
}
ISqlClause <|-- IWithClause
ISqlClause <|.. SqlClause
IWithClause <|.. WithClause
SqlClause <|-- WithClause
WithClause --> SqlClauses : uses
WithClause --> IQueryBreakdown : references
IQueryBreakdown --> SqlClauses : returns/accepts
note for WithClause "Bi-directional sync between\nSql and Query properties"
Clause Type Hierarchy
classDiagram
class ISqlClause {
<<interface>>
+string? Clause
+string? Comment
}
class ISqlExpressionClause {
<<interface>>
+IEnumerable~Expression~ GetExpressions()
}
class SqlClause {
+string? Clause
+string? Comment
}
class SqlExpressionClause {
+bool SplitOnComma
+IEnumerable~Expression~ GetExpressions()
}
class WithClause {
+string TableName
+SqlClauses? Sql
+IQueryBreakdown? Query
}
ISqlClause <|-- ISqlExpressionClause
ISqlClause <|.. SqlClause
ISqlExpressionClause <|.. SqlExpressionClause
SqlClause <|-- SqlExpressionClause
SqlClause <|-- WithClause
ISqlClause <|-- IWithClause
IWithClause <|.. WithClause
Code Examples
Usage Pattern
Creating SQL Server Query Breakdown
using Strata.SqlTools.Breakdowns.SqlServer;
var query = new QueryBreakdown();
query.SelectClause.Clause = "column1, column2";
query.FromClause.Clause = "myTable";
// AddWhereClause automatically extracts parameters
query.AddWhereClause("id = @id");
// Parameter @id is now in query.Parameters with null value
query.SetParameterValue("@id", 123);
string sql = query.GetSql(); // Returns T-SQL formatted query
Creating Query with Common Table Expression (CTE)
using Strata.SqlTools.Breakdowns.SqlServer;
// Create inner CTE query
var cteQuery = new QueryBreakdown("id, name, active", "users", "active = 1");
cteQuery.AddParameter("@minDate", DateTime.Today.AddDays(-30));
// Create main query that uses the CTE
var mainQuery = new QueryBreakdown("*", "active_users");
mainQuery.AddWithClause("active_users", cteQuery);
string sql = mainQuery.GetSql();
/* Generates:
WITH active_users AS (
SELECT id, name, active
FROM users
WHERE active = 1
)
SELECT *
FROM active_users
*/
Creating Snowflake Query Breakdown
using Strata.SqlTools.Breakdowns.Snowflake;
var query = new QueryBreakdown();
query.SelectClause.Clause = "column1, column2";
query.FromClause.Clause = "myTable";
// AddWhereClause automatically extracts parameters (supports both :param and @param)
query.AddWhereClause("id = :id", false); // false = Snowflake parsing
// Parameter :id is now in query.Parameters with null value
query.SetParameterValue(":id", 123);
string sql = query.GetSql(); // Returns Snowflake formatted query
Parsing SQL Statements
using Strata.SqlTools.Statements.SqlServer;
var parser = new StatementParser();
string normalized = parser.NormalizeSql(rawSql);
string cleaned = parser.RemoveSqlComments(normalized);
// For Snowflake
using SnowflakeParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
var snowflakeParser = new SnowflakeParser();
string snowflakeSql = snowflakeParser.NormalizeSql(rawSql); // Handles :params and "identifiers"
Tokenizing SQL
using Strata.SqlTools.Statements.SqlServer;
var reader = new StatementReader("SELECT [column1] FROM [table1]");
while (reader.Read())
{
Console.WriteLine($"{reader.TokenType}: {reader.TokenValue}");
}
// For Snowflake double-quoted identifiers
using Strata.SqlTools.Statements.Snowflake;
var snowflakeReader = new StatementReader("SELECT \"column1\" FROM \"table1\"");
Snowflake-Specific Features
The Snowflake implementations add these dialect-specific capabilities:
1. Parameter Syntax
- SqlServer:
@parameteronly - Snowflake:
:parameterand@parameter(both supported)
2. Identifier Quoting
- SqlServer:
[identifier](square brackets) - Snowflake:
"identifier"(double quotes) and[identifier]
3. Keywords
- SqlServer: Standard T-SQL keywords
- Snowflake: Additional
QUALIFYandLIMITkeywords
4. Setup/Finish Clauses
- Snowflake-specific:
ALTER SESSION,CREATE STAGE,DROP STAGE - Used for session configuration and temporary objects
Extensibility: Adding New SQL Dialects
The current architecture makes it easy to add new SQL dialects (PostgreSQL, MySQL, Oracle, etc.):
Steps to Add a New Dialect
-
Create new namespace:
Strata.SqlTools.PostgreSQL -
Inherit from SqlServer base classes:
namespace Strata.SqlTools.PostgreSQL; public class StatementParser : SqlServer.StatementParser { // Override only PostgreSQL-specific behavior } public class StatementReader : SqlServer.StatementReader { // Override tokenization for PostgreSQL-specific syntax } public class QueryBreakdown : SqlServer.QueryBreakdown { // Override query generation for PostgreSQL } -
Override only dialect-specific methods:
- Don't duplicate common SQL logic
- Call
base.Method()where appropriate - Add dialect-specific constants/keywords
-
Document differences:
- Add XML comments explaining what's dialect-specific
- Reference PostgreSQL documentation for syntax
Testing Strategy
Unit Tests Organization
- StatementReaderTests.cs - Tests SqlServer.StatementReader
- SnowflakeQueryBreakdownTests.cs - Tests Snowflake.QueryBreakdown
- Additional test files as needed for each class
Test Coverage Areas
- Tokenization: Verify correct token identification
- Parsing: Validate clause extraction and normalization
- Expression Trees: Test expression parsing accuracy
- Parameter Handling: Verify both
@paramand:paramsyntax - Identifier Quoting: Test
[brackets]and"double-quotes" - Dialect-Specific Features: Test QUALIFY, LIMIT, setup clauses
Recent Architectural Enhancements (February 2026)
Automatic Parameter Extraction (February 2026)
Enhanced AddWhereClause with intelligent parameter management:
Key Features:
- Automatic Parameter Detection - Extracts
@param(SQL Server) and:param(Snowflake) from WHERE clauses - Smart Update Logic - Type-safe parameter value management with validation
- Protected Helper Methods -
AddOrUpdateParameter()andExtractAndAddParameters()
Implementation Details:
protected void AddOrUpdateParameter(string parameterName, object? value)
{
// Normalizes parameter name (keeps : or @ prefix)
// - New parameter: Adds with provided value
// - Existing with null: Updates to new value
// - Existing with non-null same type: Keeps existing value
// - Existing with different type: Throws InvalidOperationException
}
protected void ExtractAndAddParameters(string sql)
{
// Uses StatementParser to find parameters via regex
// Calls AddOrUpdateParameter for each discovered parameter
}
Benefits:
- ✅ Automatic parameter registration when building WHERE clauses
- ✅ Type-safe parameter management prevents type mismatches
- ✅ Preserves existing parameter values during query composition
- ✅ Works seamlessly with both SQL Server (
@param) and Snowflake (:param) syntax - ✅ Reduces boilerplate - no manual
AddParametercalls needed
Usage Example:
var query = new QueryBreakdown("*", "Users");
query.AddWhereClause("UserID = @UserId AND Status = @Status");
// @UserId and @Status automatically added to Parameters dictionary
query.SetParameterValue("@UserId", 123);
query.SetParameterValue("@Status", "Active");
WITH Clause (CTE) Implementation
A comprehensive Common Table Expression (CTE) architecture was added:
Key Components:
IWithClauseInterface - Contract for CTE structureWithClauseClass - Concrete implementation with intelligent property synchronizationSqlClausesClass - Container for parsed SQL clause objects- Enhanced
IQueryBreakdown- AddedGetClauses()andApplyClauses()methods
Architecture Highlights:
- Bi-directional Synchronization:
Sql↔Queryproperties automatically sync - Query as Source of Truth: When
Queryexists,Sqlis computed from it - Polymorphic Design: No type-checking required, works with any
IQueryBreakdownimplementation - Cognitive Complexity Reduction: 68% reduction through
ApplyClauses()method extraction
Benefits:
- ✅ Structured CTE management with parameter support
- ✅ Automatic synchronization prevents stale data
- ✅ Clean API with
GetClauses()andCopy()methods - ✅ Comment preservation for CTEs
- ✅ Support for both SQL Server and Snowflake dialects
sequenceDiagram
participant User
participant WithClause
participant Query as IQueryBreakdown
Note over WithClause: Scenario: Set Query, then Get Sql
User->>WithClause: Set Query = queryBreakdown
User->>WithClause: Get Sql
WithClause->>Query: GetClauses()
Query-->>WithClause: SqlClauses (computed)
WithClause-->>User: SqlClauses
Note over WithClause: Scenario: Set Sql with existing Query
User->>WithClause: Set Sql = sqlClauses
WithClause->>Query: ApplyClauses(sqlClauses)
Note over WithClause: _sql cleared, Query is source
Summary
Current Status: ✅ WELL-ARCHITECTED
The codebase demonstrates:
- Clean separation via namespaces (SqlServer vs Snowflake)
- Proper inheritance with selective overrides
- DRY principles - shared logic in base classes
- Extensibility - easy to add new SQL dialects
- Maintainability - clear structure and delegation patterns
- Modern patterns - Interface-based design with bi-directional synchronization
- Low cognitive complexity - Method extraction and centralized logic
No Action Required
The architecture is solid and follows .NET best practices. The namespace-based organization is superior to prefix-based naming and makes the codebase easier to navigate and extend.
Future Considerations
If adding more SQL dialects:
- Continue the namespace pattern
- Inherit from SqlServer base classes (most common SQL standard)
- Override only dialect-specific behavior
- Add comprehensive unit tests for new dialect features
- Document dialect differences clearly