Skip to content

Commit 0081e36

Browse files
kyleconroyclaude
andcommitted
Add expression parsing improvements for COLLATE and pseudo columns
- Add Collation field to SimpleCaseExpression and SearchedCaseExpression - Add Collation field to ScalarSubquery for subquery COLLATE support - Handle multi-part pseudo columns (dbo.t2.$ROWGUID) correctly: - Set ColumnType to pseudo column type (PseudoColumnRowGuid, etc.) - Exclude pseudo column from MultiPartIdentifier - Add CURRENT_DATE to ParameterlessCall types - Parse COLLATE after CASE END and parenthesized subqueries Enables: BaselinesCommon_ExpressionTests, ExpressionTests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fd34163 commit 0081e36

7 files changed

Lines changed: 114 additions & 5 deletions

File tree

ast/case_expression.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package ast
44
type SearchedCaseExpression struct {
55
WhenClauses []*SearchedWhenClause
66
ElseExpression ScalarExpression
7+
Collation *Identifier
78
}
89

910
func (s *SearchedCaseExpression) node() {}
@@ -20,6 +21,7 @@ type SimpleCaseExpression struct {
2021
InputExpression ScalarExpression
2122
WhenClauses []*SimpleWhenClause
2223
ElseExpression ScalarExpression
24+
Collation *Identifier
2325
}
2426

2527
func (s *SimpleCaseExpression) node() {}

ast/nullif_coalesce.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@ type CoalesceExpression struct {
1616

1717
func (*CoalesceExpression) node() {}
1818
func (*CoalesceExpression) scalarExpression() {}
19+
20+
// ParameterlessCall represents a parameterless function call like USER, CURRENT_USER, etc.
21+
type ParameterlessCall struct {
22+
ParameterlessCallType string
23+
Collation *Identifier
24+
}
25+
26+
func (*ParameterlessCall) node() {}
27+
func (*ParameterlessCall) scalarExpression() {}

ast/scalar_subquery.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ast
33
// ScalarSubquery represents a scalar subquery expression.
44
type ScalarSubquery struct {
55
QueryExpression QueryExpression
6+
Collation *Identifier
67
}
78

89
func (s *ScalarSubquery) node() {}

parser/marshal.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,6 +2298,17 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
22982298
node["Expressions"] = exprs
22992299
}
23002300
return node
2301+
case *ast.ParameterlessCall:
2302+
node := jsonNode{
2303+
"$type": "ParameterlessCall",
2304+
}
2305+
if e.ParameterlessCallType != "" {
2306+
node["ParameterlessCallType"] = e.ParameterlessCallType
2307+
}
2308+
if e.Collation != nil {
2309+
node["Collation"] = identifierToJSON(e.Collation)
2310+
}
2311+
return node
23012312
case *ast.IdentityFunctionCall:
23022313
node := jsonNode{
23032314
"$type": "IdentityFunctionCall",
@@ -2550,6 +2561,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
25502561
if e.QueryExpression != nil {
25512562
node["QueryExpression"] = queryExpressionToJSON(e.QueryExpression)
25522563
}
2564+
if e.Collation != nil {
2565+
node["Collation"] = identifierToJSON(e.Collation)
2566+
}
25532567
return node
25542568
case *ast.SearchedCaseExpression:
25552569
node := jsonNode{
@@ -2574,6 +2588,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
25742588
if e.ElseExpression != nil {
25752589
node["ElseExpression"] = scalarExpressionToJSON(e.ElseExpression)
25762590
}
2591+
if e.Collation != nil {
2592+
node["Collation"] = identifierToJSON(e.Collation)
2593+
}
25772594
return node
25782595
case *ast.SimpleCaseExpression:
25792596
node := jsonNode{
@@ -2601,6 +2618,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
26012618
if e.ElseExpression != nil {
26022619
node["ElseExpression"] = scalarExpressionToJSON(e.ElseExpression)
26032620
}
2621+
if e.Collation != nil {
2622+
node["Collation"] = identifierToJSON(e.Collation)
2623+
}
26042624
return node
26052625
case *ast.SourceDeclaration:
26062626
node := jsonNode{

parser/parse_select.go

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,20 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
12881288
if upper == "NEXT" && strings.ToUpper(p.peekTok.Literal) == "VALUE" {
12891289
return p.parseNextValueForExpression()
12901290
}
1291+
// Check for parameterless calls (USER, CURRENT_USER, etc.) without parentheses
1292+
if p.peekTok.Type != TokenLParen {
1293+
parameterlessType := getParameterlessCallType(upper)
1294+
if parameterlessType != "" {
1295+
p.nextToken()
1296+
call := &ast.ParameterlessCall{ParameterlessCallType: parameterlessType}
1297+
// Check for optional COLLATE clause
1298+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1299+
p.nextToken() // consume COLLATE
1300+
call.Collation = p.parseIdentifier()
1301+
}
1302+
return call, nil
1303+
}
1304+
}
12911305
return p.parseColumnReferenceOrFunctionCall()
12921306
case TokenNumber:
12931307
val := p.curTok.Literal
@@ -1333,7 +1347,13 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
13331347
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
13341348
}
13351349
p.nextToken()
1336-
return &ast.ScalarSubquery{QueryExpression: qe}, nil
1350+
ss := &ast.ScalarSubquery{QueryExpression: qe}
1351+
// Check for optional COLLATE clause
1352+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1353+
p.nextToken() // consume COLLATE
1354+
ss.Collation = p.parseIdentifier()
1355+
}
1356+
return ss, nil
13371357
}
13381358
expr, err := p.parseScalarExpression()
13391359
if err != nil {
@@ -1355,7 +1375,13 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
13551375
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
13561376
}
13571377
p.nextToken()
1358-
return &ast.ScalarSubquery{QueryExpression: qe}, nil
1378+
ss := &ast.ScalarSubquery{QueryExpression: qe}
1379+
// Check for optional COLLATE clause
1380+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1381+
p.nextToken() // consume COLLATE
1382+
ss.Collation = p.parseIdentifier()
1383+
}
1384+
return ss, nil
13591385
}
13601386
}
13611387
if p.curTok.Type != TokenRParen {
@@ -1374,9 +1400,22 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
13741400
// Multi-part identifier starting with empty parts (e.g., ..t1.c1)
13751401
return p.parseColumnReferenceWithLeadingDots()
13761402
case TokenMaster, TokenDatabase, TokenKey, TokenTable, TokenIndex,
1377-
TokenSchema, TokenUser, TokenView, TokenTime:
1403+
TokenSchema, TokenView, TokenTime:
13781404
// Keywords that can be used as identifiers in column/table references
13791405
return p.parseColumnReferenceOrFunctionCall()
1406+
case TokenUser:
1407+
// USER without parentheses is a ParameterlessCall
1408+
if p.peekTok.Type != TokenLParen && p.peekTok.Type != TokenDot {
1409+
p.nextToken()
1410+
call := &ast.ParameterlessCall{ParameterlessCallType: "User"}
1411+
// Check for optional COLLATE clause
1412+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1413+
p.nextToken() // consume COLLATE
1414+
call.Collation = p.parseIdentifier()
1415+
}
1416+
return call, nil
1417+
}
1418+
return p.parseColumnReferenceOrFunctionCall()
13801419
default:
13811420
return nil, fmt.Errorf("unexpected token in expression: %s", p.curTok.Literal)
13821421
}
@@ -1436,6 +1475,12 @@ func (p *Parser) parseSearchedCaseExpression() (*ast.SearchedCaseExpression, err
14361475
}
14371476
p.nextToken() // consume END
14381477

1478+
// Check for optional COLLATE clause
1479+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1480+
p.nextToken() // consume COLLATE
1481+
expr.Collation = p.parseIdentifier()
1482+
}
1483+
14391484
return expr, nil
14401485
}
14411486

@@ -1488,6 +1533,12 @@ func (p *Parser) parseSimpleCaseExpression() (*ast.SimpleCaseExpression, error)
14881533
}
14891534
p.nextToken() // consume END
14901535

1536+
// Check for optional COLLATE clause
1537+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1538+
p.nextToken() // consume COLLATE
1539+
expr.Collation = p.parseIdentifier()
1540+
}
1541+
14911542
return expr, nil
14921543
}
14931544

@@ -1842,6 +1893,12 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err
18421893
}
18431894
p.nextToken()
18441895
break
1896+
} else if pseudoType := getPseudoColumnType(upper); pseudoType != "" {
1897+
// Pseudo columns like $ROWGUID, $IDENTITY at end of multi-part identifier
1898+
// set column type and are not included in the identifier list
1899+
colType = pseudoType
1900+
p.nextToken()
1901+
break
18451902
}
18461903

18471904
id := &ast.Identifier{
@@ -6680,6 +6737,26 @@ func getPseudoColumnType(value string) string {
66806737
}
66816738
}
66826739

6740+
// getParameterlessCallType returns the ParameterlessCallType for keywords like USER, CURRENT_USER, etc.
6741+
func getParameterlessCallType(value string) string {
6742+
switch value {
6743+
case "USER":
6744+
return "User"
6745+
case "CURRENT_USER":
6746+
return "CurrentUser"
6747+
case "SESSION_USER":
6748+
return "SessionUser"
6749+
case "SYSTEM_USER":
6750+
return "SystemUser"
6751+
case "CURRENT_TIMESTAMP":
6752+
return "CurrentTimestamp"
6753+
case "CURRENT_DATE":
6754+
return "CurrentDate"
6755+
default:
6756+
return ""
6757+
}
6758+
}
6759+
66836760
// parseFullTextPredicate parses CONTAINS or FREETEXT predicates
66846761
func (p *Parser) parseFullTextPredicate(funcType string) (*ast.FullTextPredicate, error) {
66856762
// Convert to PascalCase: "CONTAINS" -> "Contains", "FREETEXT" -> "FreeText"
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)