Skip to content

Commit 5690b37

Browse files
kyleconroyclaude
andauthored
Add UserDefinedTypePropertyAccess and OverClause parsing support (#44)
Add support for parsing user-defined type property access syntax (e.g., t::a, (c1).SomeProperty) with optional COLLATE clause, and basic OVER clause support for window functions. Key changes: - Add UserDefinedTypePropertyAccess AST type for UDT property access - Add OverClause AST type to FunctionCall - Handle :: syntax for property access (not just method calls) - Parse chained property access via ExpressionCallTarget - Parse COLLATE clause on property access expressions - Parse ALL/DISTINCT modifiers in function calls - Add JSON marshaling for new types Enables tests: - Baselines100_ExpressionTests100 - ExpressionTests100 Co-authored-by: Claude <[email protected]>
1 parent 18f0d6d commit 5690b37

File tree

6 files changed

+201
-39
lines changed

6 files changed

+201
-39
lines changed

ast/function_call.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ type UserDefinedTypeCallTarget struct {
2626

2727
func (*UserDefinedTypeCallTarget) callTarget() {}
2828

29+
// OverClause represents an OVER clause for window functions.
30+
type OverClause struct {
31+
// Add partition by, order by, and window frame as needed
32+
}
33+
2934
// FunctionCall represents a function call.
3035
type FunctionCall struct {
3136
CallTarget CallTarget `json:"CallTarget,omitempty"`
3237
FunctionName *Identifier `json:"FunctionName,omitempty"`
3338
Parameters []ScalarExpression `json:"Parameters,omitempty"`
3439
UniqueRowFilter string `json:"UniqueRowFilter,omitempty"`
40+
OverClause *OverClause `json:"OverClause,omitempty"`
3541
WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"`
3642
}
3743

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package ast
2+
3+
// UserDefinedTypePropertyAccess represents a property access on a user-defined type.
4+
// Examples: t::a, (c1).SomeProperty, c1.f1().SomeProperty
5+
type UserDefinedTypePropertyAccess struct {
6+
CallTarget CallTarget `json:"CallTarget,omitempty"`
7+
PropertyName *Identifier `json:"PropertyName,omitempty"`
8+
Collation *Identifier `json:"Collation,omitempty"`
9+
}
10+
11+
func (*UserDefinedTypePropertyAccess) node() {}
12+
func (*UserDefinedTypePropertyAccess) scalarExpression() {}

parser/marshal.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,8 +1242,27 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
12421242
if e.UniqueRowFilter != "" {
12431243
node["UniqueRowFilter"] = e.UniqueRowFilter
12441244
}
1245+
if e.OverClause != nil {
1246+
node["OverClause"] = jsonNode{
1247+
"$type": "OverClause",
1248+
}
1249+
}
12451250
node["WithArrayWrapper"] = e.WithArrayWrapper
12461251
return node
1252+
case *ast.UserDefinedTypePropertyAccess:
1253+
node := jsonNode{
1254+
"$type": "UserDefinedTypePropertyAccess",
1255+
}
1256+
if e.CallTarget != nil {
1257+
node["CallTarget"] = callTargetToJSON(e.CallTarget)
1258+
}
1259+
if e.PropertyName != nil {
1260+
node["PropertyName"] = identifierToJSON(e.PropertyName)
1261+
}
1262+
if e.Collation != nil {
1263+
node["Collation"] = identifierToJSON(e.Collation)
1264+
}
1265+
return node
12471266
case *ast.BinaryExpression:
12481267
node := jsonNode{
12491268
"$type": "BinaryExpression",

parser/parse_select.go

Lines changed: 162 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,8 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
685685
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
686686
}
687687
p.nextToken()
688-
return &ast.ParenthesisExpression{Expression: expr}, nil
688+
// Check for property access after parenthesized expression: (c1).SomeProperty
689+
return p.parsePostExpressionAccess(&ast.ParenthesisExpression{Expression: expr})
689690
case TokenCase:
690691
return p.parseCaseExpression()
691692
default:
@@ -966,58 +967,75 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err
966967
p.nextToken() // consume dot
967968
}
968969

969-
// Check for :: (user-defined type method call): a.b::func()
970+
// Check for :: (user-defined type method call or property access): a.b::func() or a::prop
970971
if p.curTok.Type == TokenColonColon && len(identifiers) > 0 {
971972
p.nextToken() // consume ::
972973

973-
// Parse function name
974+
// Parse function/property name
974975
if p.curTok.Type != TokenIdent {
975-
return nil, fmt.Errorf("expected function name after ::, got %s", p.curTok.Literal)
976+
return nil, fmt.Errorf("expected identifier after ::, got %s", p.curTok.Literal)
976977
}
977-
funcName := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
978+
name := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
978979
p.nextToken()
979980

980-
// Expect (
981-
if p.curTok.Type != TokenLParen {
982-
return nil, fmt.Errorf("expected ( after function name, got %s", p.curTok.Literal)
983-
}
984-
p.nextToken() // consume (
985-
986981
// Build SchemaObjectName from identifiers
987982
schemaObjName := identifiersToSchemaObjectName(identifiers)
988983

989-
fc := &ast.FunctionCall{
990-
CallTarget: &ast.UserDefinedTypeCallTarget{
991-
SchemaObjectName: schemaObjName,
992-
},
993-
FunctionName: funcName,
994-
UniqueRowFilter: "NotSpecified",
995-
WithArrayWrapper: false,
996-
}
984+
// If followed by ( it's a method call, otherwise property access
985+
if p.curTok.Type == TokenLParen {
986+
p.nextToken() // consume (
987+
988+
fc := &ast.FunctionCall{
989+
CallTarget: &ast.UserDefinedTypeCallTarget{
990+
SchemaObjectName: schemaObjName,
991+
},
992+
FunctionName: name,
993+
UniqueRowFilter: "NotSpecified",
994+
WithArrayWrapper: false,
995+
}
997996

998-
// Parse parameters
999-
if p.curTok.Type != TokenRParen {
1000-
for {
1001-
param, err := p.parseScalarExpression()
1002-
if err != nil {
1003-
return nil, err
1004-
}
1005-
fc.Parameters = append(fc.Parameters, param)
997+
// Parse parameters
998+
if p.curTok.Type != TokenRParen {
999+
for {
1000+
param, err := p.parseScalarExpression()
1001+
if err != nil {
1002+
return nil, err
1003+
}
1004+
fc.Parameters = append(fc.Parameters, param)
10061005

1007-
if p.curTok.Type != TokenComma {
1008-
break
1006+
if p.curTok.Type != TokenComma {
1007+
break
1008+
}
1009+
p.nextToken() // consume comma
10091010
}
1010-
p.nextToken() // consume comma
10111011
}
1012+
1013+
// Expect )
1014+
if p.curTok.Type != TokenRParen {
1015+
return nil, fmt.Errorf("expected ) in function call, got %s", p.curTok.Literal)
1016+
}
1017+
p.nextToken()
1018+
1019+
// Check for OVER clause or property access after method call
1020+
return p.parsePostExpressionAccess(fc)
10121021
}
10131022

1014-
// Expect )
1015-
if p.curTok.Type != TokenRParen {
1016-
return nil, fmt.Errorf("expected ) in function call, got %s", p.curTok.Literal)
1023+
// Property access: t::a
1024+
propAccess := &ast.UserDefinedTypePropertyAccess{
1025+
CallTarget: &ast.UserDefinedTypeCallTarget{
1026+
SchemaObjectName: schemaObjName,
1027+
},
1028+
PropertyName: name,
1029+
}
1030+
1031+
// Check for COLLATE clause
1032+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1033+
p.nextToken() // consume COLLATE
1034+
propAccess.Collation = p.parseIdentifier()
10171035
}
1018-
p.nextToken()
10191036

1020-
return fc, nil
1037+
// Check for chained property access
1038+
return p.parsePostExpressionAccess(propAccess)
10211039
}
10221040

10231041
// If followed by ( it's a function call
@@ -1046,7 +1064,7 @@ func (p *Parser) parseColumnReference() (*ast.ColumnReferenceExpression, error)
10461064
return nil, fmt.Errorf("expected column reference, got function call")
10471065
}
10481066

1049-
func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (*ast.FunctionCall, error) {
1067+
func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (ast.ScalarExpression, error) {
10501068
fc := &ast.FunctionCall{
10511069
UniqueRowFilter: "NotSpecified",
10521070
WithArrayWrapper: false,
@@ -1070,6 +1088,15 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier)
10701088
// Consume (
10711089
p.nextToken()
10721090

1091+
// Check for ALL or DISTINCT
1092+
if strings.ToUpper(p.curTok.Literal) == "ALL" {
1093+
fc.UniqueRowFilter = "All"
1094+
p.nextToken()
1095+
} else if strings.ToUpper(p.curTok.Literal) == "DISTINCT" {
1096+
fc.UniqueRowFilter = "Distinct"
1097+
p.nextToken()
1098+
}
1099+
10731100
// Parse parameters
10741101
if p.curTok.Type != TokenRParen {
10751102
for {
@@ -1092,7 +1119,105 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier)
10921119
}
10931120
p.nextToken()
10941121

1095-
return fc, nil
1122+
// Check for OVER clause or property access after function call
1123+
return p.parsePostExpressionAccess(fc)
1124+
}
1125+
1126+
// parsePostExpressionAccess handles chained property access (.PropertyName), COLLATE clauses, and OVER clauses
1127+
// after an expression (function call, parenthesized expression, or property access).
1128+
func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.ScalarExpression, error) {
1129+
// Loop to handle chained property access like .SomeProperty.AnotherProperty
1130+
for {
1131+
// Check for .PropertyName pattern (property access)
1132+
if p.curTok.Type == TokenDot {
1133+
p.nextToken() // consume .
1134+
1135+
if p.curTok.Type != TokenIdent {
1136+
return nil, fmt.Errorf("expected property name after ., got %s", p.curTok.Literal)
1137+
}
1138+
propName := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
1139+
p.nextToken()
1140+
1141+
// Check if it's a method call: .method()
1142+
if p.curTok.Type == TokenLParen {
1143+
p.nextToken() // consume (
1144+
1145+
fc := &ast.FunctionCall{
1146+
CallTarget: &ast.ExpressionCallTarget{
1147+
Expression: expr,
1148+
},
1149+
FunctionName: propName,
1150+
UniqueRowFilter: "NotSpecified",
1151+
WithArrayWrapper: false,
1152+
}
1153+
1154+
// Parse parameters
1155+
if p.curTok.Type != TokenRParen {
1156+
for {
1157+
param, err := p.parseScalarExpression()
1158+
if err != nil {
1159+
return nil, err
1160+
}
1161+
fc.Parameters = append(fc.Parameters, param)
1162+
1163+
if p.curTok.Type != TokenComma {
1164+
break
1165+
}
1166+
p.nextToken() // consume comma
1167+
}
1168+
}
1169+
1170+
// Expect )
1171+
if p.curTok.Type != TokenRParen {
1172+
return nil, fmt.Errorf("expected ) in method call, got %s", p.curTok.Literal)
1173+
}
1174+
p.nextToken()
1175+
1176+
expr = fc
1177+
continue
1178+
}
1179+
1180+
// Property access: .PropertyName
1181+
propAccess := &ast.UserDefinedTypePropertyAccess{
1182+
CallTarget: &ast.ExpressionCallTarget{
1183+
Expression: expr,
1184+
},
1185+
PropertyName: propName,
1186+
}
1187+
1188+
// Check for COLLATE clause
1189+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
1190+
p.nextToken() // consume COLLATE
1191+
propAccess.Collation = p.parseIdentifier()
1192+
}
1193+
1194+
expr = propAccess
1195+
continue
1196+
}
1197+
1198+
// Check for OVER clause for function calls
1199+
if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "OVER" {
1200+
p.nextToken() // consume OVER
1201+
1202+
if p.curTok.Type != TokenLParen {
1203+
return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal)
1204+
}
1205+
p.nextToken() // consume (
1206+
1207+
// For now, just skip to closing paren (basic OVER() support)
1208+
// TODO: Parse partition by, order by, and window frame
1209+
if p.curTok.Type != TokenRParen {
1210+
return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal)
1211+
}
1212+
p.nextToken() // consume )
1213+
1214+
fc.OverClause = &ast.OverClause{}
1215+
}
1216+
1217+
break
1218+
}
1219+
1220+
return expr, nil
10961221
}
10971222

10981223
func (p *Parser) parseFromClause() (*ast.FromClause, error) {
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)