Skip to content

Commit 89e0207

Browse files
committed
Add BooleanScalarPlaceholder for parenthesized scalar expressions
Fixes parsing of patterns like `IF (XACT_STATE()) = -1` where a scalar expression is parenthesized in a boolean context. The parser now returns a BooleanScalarPlaceholder when encountering a closing paren without a comparison operator, allowing the parent to handle it correctly.
1 parent 686cb2b commit 89e0207

4 files changed

Lines changed: 230 additions & 2 deletions

File tree

ast/boolean_expression.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,13 @@ type BooleanExpression interface {
55
Node
66
booleanExpression()
77
}
8+
9+
// BooleanScalarPlaceholder is a temporary marker used during parsing when we
10+
// encounter a scalar expression in a boolean context without a comparison operator.
11+
// This allows the caller to detect and handle cases like (XACT_STATE()) = -1.
12+
type BooleanScalarPlaceholder struct {
13+
Scalar ScalarExpression
14+
}
15+
16+
func (b *BooleanScalarPlaceholder) booleanExpression() {}
17+
func (b *BooleanScalarPlaceholder) node() {}

parser/parse_select.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,6 +2814,54 @@ func (p *Parser) parseBooleanPrimaryExpression() (ast.BooleanExpression, error)
28142814
return nil, err
28152815
}
28162816

2817+
// Check if we got a placeholder for a scalar expression without comparison
2818+
// This happens when parsing something like (XACT_STATE()) in: IF (XACT_STATE()) = -1
2819+
if placeholder, ok := inner.(*ast.BooleanScalarPlaceholder); ok {
2820+
// The inner content was a bare scalar expression
2821+
// curTok should still be ) since we didn't consume it
2822+
if p.curTok.Type != TokenRParen {
2823+
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
2824+
}
2825+
p.nextToken() // consume )
2826+
2827+
// Wrap the scalar in a ParenthesisExpression
2828+
parenExpr := &ast.ParenthesisExpression{Expression: placeholder.Scalar}
2829+
2830+
// Check for comparison operators after the parenthesized expression
2831+
if p.isComparisonOperator() {
2832+
return p.parseComparisonAfterLeft(parenExpr)
2833+
}
2834+
2835+
// Check for IS NULL / IS NOT NULL
2836+
if p.curTok.Type == TokenIs {
2837+
return p.parseIsNullAfterLeft(parenExpr)
2838+
}
2839+
2840+
// Check for NOT before IN/LIKE/BETWEEN
2841+
notDefined := false
2842+
if p.curTok.Type == TokenNot {
2843+
notDefined = true
2844+
p.nextToken()
2845+
}
2846+
2847+
if p.curTok.Type == TokenIn {
2848+
return p.parseInExpressionAfterLeft(parenExpr, notDefined)
2849+
}
2850+
if p.curTok.Type == TokenLike {
2851+
return p.parseLikeExpressionAfterLeft(parenExpr, notDefined)
2852+
}
2853+
if p.curTok.Type == TokenBetween {
2854+
return p.parseBetweenExpressionAfterLeft(parenExpr, notDefined)
2855+
}
2856+
2857+
if notDefined {
2858+
return nil, fmt.Errorf("expected IN, LIKE, or BETWEEN after NOT, got %s", p.curTok.Literal)
2859+
}
2860+
2861+
// If no comparison follows, return error
2862+
return nil, fmt.Errorf("expected comparison operator after parenthesized expression, got %s", p.curTok.Literal)
2863+
}
2864+
28172865
if p.curTok.Type != TokenRParen {
28182866
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
28192867
}
@@ -2983,6 +3031,11 @@ func (p *Parser) parseBooleanPrimaryExpression() (ast.BooleanExpression, error)
29833031
compType = "LessThanOrEqualTo"
29843032
case TokenGreaterOrEqual:
29853033
compType = "GreaterThanOrEqualTo"
3034+
case TokenRParen:
3035+
// We're at ) without a comparison operator - this happens when parsing
3036+
// a parenthesized scalar expression like (XACT_STATE()) in a boolean context.
3037+
// Return a special marker that the caller can handle.
3038+
return &ast.BooleanScalarPlaceholder{Scalar: left}, nil
29863039
default:
29873040
return nil, fmt.Errorf("expected comparison operator, got %s", p.curTok.Literal)
29883041
}
@@ -3046,6 +3099,171 @@ func (p *Parser) parseComparisonAfterLeft(left ast.ScalarExpression) (ast.Boolea
30463099
}, nil
30473100
}
30483101

3102+
// parseInExpressionAfterLeft parses an IN expression after the left operand is already parsed
3103+
func (p *Parser) parseInExpressionAfterLeft(left ast.ScalarExpression, notDefined bool) (ast.BooleanExpression, error) {
3104+
p.nextToken() // consume IN
3105+
3106+
if p.curTok.Type != TokenLParen {
3107+
return nil, fmt.Errorf("expected ( after IN, got %s", p.curTok.Literal)
3108+
}
3109+
p.nextToken() // consume (
3110+
3111+
// Check if it's a subquery or value list
3112+
if p.curTok.Type == TokenSelect {
3113+
subquery, err := p.parseQueryExpression()
3114+
if err != nil {
3115+
return nil, err
3116+
}
3117+
if p.curTok.Type != TokenRParen {
3118+
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
3119+
}
3120+
p.nextToken() // consume )
3121+
return &ast.BooleanInExpression{
3122+
Expression: left,
3123+
NotDefined: notDefined,
3124+
Subquery: subquery,
3125+
}, nil
3126+
}
3127+
3128+
// Parse value list
3129+
var values []ast.ScalarExpression
3130+
for {
3131+
val, err := p.parseScalarExpression()
3132+
if err != nil {
3133+
return nil, err
3134+
}
3135+
values = append(values, val)
3136+
if p.curTok.Type != TokenComma {
3137+
break
3138+
}
3139+
p.nextToken() // consume ,
3140+
}
3141+
if p.curTok.Type != TokenRParen {
3142+
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
3143+
}
3144+
p.nextToken() // consume )
3145+
return &ast.BooleanInExpression{
3146+
Expression: left,
3147+
NotDefined: notDefined,
3148+
Values: values,
3149+
}, nil
3150+
}
3151+
3152+
// parseLikeExpressionAfterLeft parses a LIKE expression after the left operand is already parsed
3153+
func (p *Parser) parseLikeExpressionAfterLeft(left ast.ScalarExpression, notDefined bool) (ast.BooleanExpression, error) {
3154+
p.nextToken() // consume LIKE
3155+
3156+
pattern, err := p.parseScalarExpression()
3157+
if err != nil {
3158+
return nil, err
3159+
}
3160+
3161+
var escapeExpr ast.ScalarExpression
3162+
if p.curTok.Type == TokenEscape {
3163+
p.nextToken() // consume ESCAPE
3164+
escapeExpr, err = p.parseScalarExpression()
3165+
if err != nil {
3166+
return nil, err
3167+
}
3168+
}
3169+
3170+
return &ast.BooleanLikeExpression{
3171+
FirstExpression: left,
3172+
SecondExpression: pattern,
3173+
EscapeExpression: escapeExpr,
3174+
NotDefined: notDefined,
3175+
}, nil
3176+
}
3177+
3178+
// parseBetweenExpressionAfterLeft parses a BETWEEN expression after the left operand is already parsed
3179+
func (p *Parser) parseBetweenExpressionAfterLeft(left ast.ScalarExpression, notDefined bool) (ast.BooleanExpression, error) {
3180+
p.nextToken() // consume BETWEEN
3181+
3182+
low, err := p.parseScalarExpression()
3183+
if err != nil {
3184+
return nil, err
3185+
}
3186+
3187+
if p.curTok.Type != TokenAnd {
3188+
return nil, fmt.Errorf("expected AND in BETWEEN, got %s", p.curTok.Literal)
3189+
}
3190+
p.nextToken() // consume AND
3191+
3192+
high, err := p.parseScalarExpression()
3193+
if err != nil {
3194+
return nil, err
3195+
}
3196+
3197+
ternaryType := "Between"
3198+
if notDefined {
3199+
ternaryType = "NotBetween"
3200+
}
3201+
return &ast.BooleanTernaryExpression{
3202+
TernaryExpressionType: ternaryType,
3203+
FirstExpression: left,
3204+
SecondExpression: low,
3205+
ThirdExpression: high,
3206+
}, nil
3207+
}
3208+
3209+
// finishParenthesizedBooleanExpression finishes parsing a parenthesized boolean expression
3210+
// after the initial comparison/expression has been parsed
3211+
func (p *Parser) finishParenthesizedBooleanExpression(inner ast.BooleanExpression) (ast.BooleanExpression, error) {
3212+
// Check for AND/OR continuation
3213+
for p.curTok.Type == TokenAnd || p.curTok.Type == TokenOr {
3214+
op := p.curTok.Type
3215+
p.nextToken()
3216+
3217+
right, err := p.parseBooleanPrimaryExpression()
3218+
if err != nil {
3219+
return nil, err
3220+
}
3221+
3222+
if op == TokenAnd {
3223+
inner = &ast.BooleanBinaryExpression{
3224+
BinaryExpressionType: "And",
3225+
FirstExpression: inner,
3226+
SecondExpression: right,
3227+
}
3228+
} else {
3229+
inner = &ast.BooleanBinaryExpression{
3230+
BinaryExpressionType: "Or",
3231+
FirstExpression: inner,
3232+
SecondExpression: right,
3233+
}
3234+
}
3235+
}
3236+
3237+
// Expect closing parenthesis
3238+
if p.curTok.Type != TokenRParen {
3239+
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
3240+
}
3241+
p.nextToken() // consume )
3242+
3243+
return &ast.BooleanParenthesisExpression{Expression: inner}, nil
3244+
}
3245+
3246+
// parseIsNullAfterLeft parses IS NULL / IS NOT NULL after the left operand is already parsed
3247+
func (p *Parser) parseIsNullAfterLeft(left ast.ScalarExpression) (ast.BooleanExpression, error) {
3248+
p.nextToken() // consume IS
3249+
3250+
isNot := false
3251+
if p.curTok.Type == TokenNot {
3252+
isNot = true
3253+
p.nextToken() // consume NOT
3254+
}
3255+
3256+
if p.curTok.Type != TokenNull {
3257+
return nil, fmt.Errorf("expected NULL after IS/IS NOT, got %s", p.curTok.Literal)
3258+
}
3259+
p.nextToken() // consume NULL
3260+
3261+
return &ast.BooleanIsNullExpression{
3262+
IsNot: isNot,
3263+
Expression: left,
3264+
}, nil
3265+
}
3266+
30493267
// identifiersToSchemaObjectName converts a slice of identifiers to a SchemaObjectName.
30503268
// For 1 identifier: BaseIdentifier
30513269
// For 2 identifiers: SchemaIdentifier.BaseIdentifier
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)