This guide documents common patterns for extending the SqlScriptDOM parser grammar to support new syntax or enhance existing functionality.
When existing grammar rules only accept literal values but need to support dynamic expressions like parameters, variables, or computed values.
Functions or constructs that currently accept only:
IntegerLiteral(e.g.,TOP_N = 10)StringLiteral(e.g.,VALUE = 'literal')
But need to support:
- Parameters:
@parameter - Variables:
@variable - Column references:
table.column - Outer references:
outerref.column - Function calls:
FUNCTION(args) - Computed expressions:
value + 1
DO NOT modify existing shared grammar rules like identifierColumnReferenceExpression that are used throughout the codebase. This can cause unintended side effects and break other functionality.
Instead, create specialized rules for your specific context.
<!-- Before: -->
<Member Name="PropertyName" Type="IntegerLiteral" Summary="Description" />
<!-- After: -->
<Member Name="PropertyName" Type="ScalarExpression" Summary="Description" />// Create a specialized rule for your context
yourContextColumnReferenceExpression returns [ColumnReferenceExpression vResult = this.FragmentFactory.CreateFragment<ColumnReferenceExpression>()]
{
MultiPartIdentifier vMultiPartIdentifier;
}
:
vMultiPartIdentifier=multiPartIdentifier[2] // Allows table.column syntax
{
vResult.ColumnType = ColumnType.Regular;
vResult.MultiPartIdentifier = vMultiPartIdentifier;
}
;
// Use the specialized rule in your custom grammar
yourContextParameterRule returns [ScalarExpression vResult]
: vResult=signedInteger
| vResult=variable
| vResult=yourContextColumnReferenceExpression // Context-specific rule
;Most script generators using GenerateNameEqualsValue() or similar methods work automatically with ScalarExpression. No changes typically needed.
Add tests within the existing test framework:
[TestMethod]
public void VerifyGrammarExtension()
{
var parser = new TSql170Parser(true);
// Test parameter
var sql1 = "SELECT FUNCTION_NAME(PARAM = @parameter)";
var result1 = parser.Parse(new StringReader(sql1), out var errors1);
Assert.AreEqual(0, errors1.Count, "Parameter syntax should work");
// Test outer reference
var sql2 = "SELECT FUNCTION_NAME(PARAM = outerref.column)";
var result2 = parser.Parse(new StringReader(sql2), out var errors2);
Assert.AreEqual(0, errors2.Count, "Outer reference syntax should work");
// Test computed expression
var sql3 = "SELECT FUNCTION_NAME(PARAM = value + 1)";
var result3 = parser.Parse(new StringReader(sql3), out var errors3);
Assert.AreEqual(0, errors3.Count, "Computed expression syntax should work");
}Only170SyntaxTests.cs). Never create standalone test projects.
Problem: VECTOR_SEARCH TOP_N parameter only accepted integer literals.
❌ Wrong Approach: Modify identifierColumnReferenceExpression to use multiPartIdentifier[2]
- Result: Broke
CreateIndexStatementErrorTestbecause other grammar rules started accepting invalid syntax
✅ Correct Approach: Create vectorSearchColumnReferenceExpression specialized for VECTOR_SEARCH
- Result: VECTOR_SEARCH supports multi-part identifiers without affecting other functionality
Final Implementation:
signedIntegerOrVariableOrColumnReference returns [ScalarExpression vResult]
: vResult=signedInteger
| vResult=variable
| vResult=vectorSearchColumnReferenceExpression // VECTOR_SEARCH-specific rule
;
vectorSearchColumnReferenceExpression returns [ColumnReferenceExpression vResult = ...]
:
vMultiPartIdentifier=multiPartIdentifier[2] // Allows table.column syntax
{
vResult.ColumnType = ColumnType.Regular;
vResult.MultiPartIdentifier = vMultiPartIdentifier;
}
;Result: Now supports dynamic TOP_N values:
-- Parameters
VECTOR_SEARCH(..., TOP_N = @k) AS ann
-- Outer references
VECTOR_SEARCH(..., TOP_N = outerref.max_results) AS annWhen adding new operators, keywords, or options to existing constructs.
<Enum Name="ExistingEnumType">
<Member Name="ExistingValue1" />
<Member Name="ExistingValue2" />
<Member Name="NewValue" /> <!-- Add this -->
</Enum>// Add new token matching
| tNewValue:Identifier
{
Match(tNewValue, CodeGenerationSupporter.NewValue);
vResult.EnumProperty = ExistingEnumType.NewValue;
}// Add mapping in appropriate generator file
private static readonly Dictionary<EnumType, string> _enumGenerators =
new Dictionary<EnumType, string>()
{
{ EnumType.ExistingValue1, CodeGenerationSupporter.ExistingValue1 },
{ EnumType.ExistingValue2, CodeGenerationSupporter.ExistingValue2 },
{ EnumType.NewValue, CodeGenerationSupporter.NewValue }, // Add this
};When adding completely new T-SQL functions or statements.
<Class Name="NewFunctionCall" Base="PrimaryExpression">
<Member Name="Parameter1" Type="ScalarExpression" />
<Member Name="Parameter2" Type="StringLiteral" />
</Class>newFunctionCall returns [NewFunctionCall vResult = FragmentFactory.CreateFragment<NewFunctionCall>()]
{
ScalarExpression vParam1;
StringLiteral vParam2;
}
:
tFunction:Identifier LeftParenthesis
{
Match(tFunction, CodeGenerationSupporter.NewFunction);
UpdateTokenInfo(vResult, tFunction);
}
vParam1 = expression
{
vResult.Parameter1 = vParam1;
}
Comma vParam2 = stringLiteral
{
vResult.Parameter2 = vParam2;
}
RightParenthesis
;Add the new rule to appropriate places in the grammar (e.g., functionCall, primaryExpression, etc.).
public override void ExplicitVisit(NewFunctionCall node)
{
GenerateIdentifier(CodeGenerationSupporter.NewFunction);
GenerateSymbol(TSqlTokenType.LeftParenthesis);
GenerateFragmentIfNotNull(node.Parameter1);
GenerateSymbol(TSqlTokenType.Comma);
GenerateFragmentIfNotNull(node.Parameter2);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}- Always ensure existing syntax continues to work
- Extend rather than replace existing rules
- Test both old and new syntax
- Add comprehensive test cases in
TestScripts/ - Update baseline files with expected output
- Test edge cases and error conditions
- Update grammar comments with new syntax
- Add examples in code comments
- Document any limitations or requirements
- Add new features to the appropriate SQL Server version grammar
- Consider whether feature should be backported to earlier versions
- Update all relevant grammar files if syntax is version-independent
- Grammar changes often require corresponding script generator changes
- Test the round-trip: parse → generate → parse again
- Test all supported expression types when extending to
ScalarExpression - Include error cases and boundary conditions
- New syntax should be added to all relevant grammar versions
- Consider SQL Server version compatibility
- Choose appropriate base classes for new AST nodes
- Consider reusing existing AST patterns where possible
- Ensure proper inheritance hierarchy
- VECTOR_SEARCH TOP_N Extension: Literal to expression pattern
- REGEXP_LIKE Predicate: Boolean parentheses recognition pattern
- EVENT SESSION Predicates: Function-style vs operator-style predicates
For detailed step-by-step examples, see BUG_FIXING_GUIDE.md.