Skip to content

Commit 628f301

Browse files
kyleconroyclaude
andcommitted
Add comprehensive boolean expression parsing support
- Add TSEQUAL predicate parsing for timestamp comparisons - Add UPDATE() predicate support for triggers - Add != !< !> T-SQL specific comparison operators - Add ~ (bitwise NOT) operator support - Add ODBC escape syntax {ESCAPE 'x'} for LIKE expressions - Fix CHECK NOT FOR REPLICATION at table level - Fix FREETEXT/CONTAINS with table.* and table.column syntax - Fix pseudo column ($identity, $action, etc.) handling in full-text predicates - Change WildcardColumn to Wildcard for consistency Enables BooleanExpressionTests and BaselinesCommon_BooleanExpressionTests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8ed22c5 commit 628f301

7 files changed

Lines changed: 358 additions & 19 deletions

File tree

ast/tsequal_call.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package ast
2+
3+
// TSEqualCall represents the TSEQUAL(expr1, expr2) predicate
4+
// used to compare timestamp values
5+
type TSEqualCall struct {
6+
FirstExpression ScalarExpression
7+
SecondExpression ScalarExpression
8+
}
9+
10+
func (*TSEqualCall) node() {}
11+
func (*TSEqualCall) expression() {}
12+
func (*TSEqualCall) booleanExpression() {}

ast/update_call.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ast
2+
3+
// UpdateCall represents the UPDATE(column) predicate used in triggers
4+
// to check if a column was modified
5+
type UpdateCall struct {
6+
Identifier *Identifier
7+
}
8+
9+
func (*UpdateCall) node() {}
10+
func (*UpdateCall) expression() {}
11+
func (*UpdateCall) booleanExpression() {}

parser/marshal.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3407,6 +3407,25 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode {
34073407
node["Expression"] = booleanExpressionToJSON(e.Expression)
34083408
}
34093409
return node
3410+
case *ast.UpdateCall:
3411+
node := jsonNode{
3412+
"$type": "UpdateCall",
3413+
}
3414+
if e.Identifier != nil {
3415+
node["Identifier"] = identifierToJSON(e.Identifier)
3416+
}
3417+
return node
3418+
case *ast.TSEqualCall:
3419+
node := jsonNode{
3420+
"$type": "TSEqualCall",
3421+
}
3422+
if e.FirstExpression != nil {
3423+
node["FirstExpression"] = scalarExpressionToJSON(e.FirstExpression)
3424+
}
3425+
if e.SecondExpression != nil {
3426+
node["SecondExpression"] = scalarExpressionToJSON(e.SecondExpression)
3427+
}
3428+
return node
34103429
case *ast.BooleanIsNullExpression:
34113430
node := jsonNode{
34123431
"$type": "BooleanIsNullExpression",
@@ -3547,7 +3566,10 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode {
35473566
"$type": "ExistsPredicate",
35483567
}
35493568
if e.Subquery != nil {
3550-
node["Subquery"] = queryExpressionToJSON(e.Subquery)
3569+
node["Subquery"] = jsonNode{
3570+
"$type": "ScalarSubquery",
3571+
"QueryExpression": queryExpressionToJSON(e.Subquery),
3572+
}
35513573
}
35523574
return node
35533575
case *ast.GraphMatchCompositeExpression:
@@ -7018,6 +7040,18 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) {
70187040
col.DefaultConstraint = defaultConstraint
70197041
} else if upperLit == "CHECK" {
70207042
p.nextToken() // consume CHECK
7043+
notForReplication := false
7044+
// Check for NOT FOR REPLICATION (comes before the condition)
7045+
if p.curTok.Type == TokenNot {
7046+
p.nextToken() // consume NOT
7047+
if strings.ToUpper(p.curTok.Literal) == "FOR" {
7048+
p.nextToken() // consume FOR
7049+
if strings.ToUpper(p.curTok.Literal) == "REPLICATION" {
7050+
p.nextToken() // consume REPLICATION
7051+
notForReplication = true
7052+
}
7053+
}
7054+
}
70217055
if p.curTok.Type == TokenLParen {
70227056
p.nextToken() // consume (
70237057
cond, err := p.parseBooleanExpression()
@@ -7030,6 +7064,7 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) {
70307064
col.Constraints = append(col.Constraints, &ast.CheckConstraintDefinition{
70317065
CheckCondition: cond,
70327066
ConstraintIdentifier: constraintName,
7067+
NotForReplication: notForReplication,
70337068
})
70347069
constraintName = nil // clear for next constraint
70357070
}
@@ -7845,13 +7880,25 @@ func (p *Parser) parseForeignKeyAction() string {
78457880
}
78467881
}
78477882

7848-
// parseCheckConstraint parses CHECK (expression)
7883+
// parseCheckConstraint parses CHECK (expression) or CHECK NOT FOR REPLICATION (expression)
78497884
func (p *Parser) parseCheckConstraint() (*ast.CheckConstraintDefinition, error) {
78507885
// Consume CHECK
78517886
p.nextToken()
78527887

78537888
constraint := &ast.CheckConstraintDefinition{}
78547889

7890+
// Check for NOT FOR REPLICATION (comes before the condition)
7891+
if p.curTok.Type == TokenNot {
7892+
p.nextToken() // consume NOT
7893+
if strings.ToUpper(p.curTok.Literal) == "FOR" {
7894+
p.nextToken() // consume FOR
7895+
if strings.ToUpper(p.curTok.Literal) == "REPLICATION" {
7896+
p.nextToken() // consume REPLICATION
7897+
constraint.NotForReplication = true
7898+
}
7899+
}
7900+
}
7901+
78557902
// Parse condition
78567903
if p.curTok.Type == TokenLParen {
78577904
p.nextToken() // consume (

0 commit comments

Comments
 (0)