Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ast/function_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ type UserDefinedTypeCallTarget struct {

func (*UserDefinedTypeCallTarget) callTarget() {}

// OverClause represents an OVER clause for window functions.
type OverClause struct {
// Add partition by, order by, and window frame as needed
}

// FunctionCall represents a function call.
type FunctionCall struct {
CallTarget CallTarget `json:"CallTarget,omitempty"`
FunctionName *Identifier `json:"FunctionName,omitempty"`
Parameters []ScalarExpression `json:"Parameters,omitempty"`
UniqueRowFilter string `json:"UniqueRowFilter,omitempty"`
OverClause *OverClause `json:"OverClause,omitempty"`
WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"`
}

Expand Down
12 changes: 12 additions & 0 deletions ast/user_defined_type_property_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ast

// UserDefinedTypePropertyAccess represents a property access on a user-defined type.
// Examples: t::a, (c1).SomeProperty, c1.f1().SomeProperty
type UserDefinedTypePropertyAccess struct {
CallTarget CallTarget `json:"CallTarget,omitempty"`
PropertyName *Identifier `json:"PropertyName,omitempty"`
Collation *Identifier `json:"Collation,omitempty"`
}

func (*UserDefinedTypePropertyAccess) node() {}
func (*UserDefinedTypePropertyAccess) scalarExpression() {}
19 changes: 19 additions & 0 deletions parser/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -1242,8 +1242,27 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
if e.UniqueRowFilter != "" {
node["UniqueRowFilter"] = e.UniqueRowFilter
}
if e.OverClause != nil {
node["OverClause"] = jsonNode{
"$type": "OverClause",
}
}
node["WithArrayWrapper"] = e.WithArrayWrapper
return node
case *ast.UserDefinedTypePropertyAccess:
node := jsonNode{
"$type": "UserDefinedTypePropertyAccess",
}
if e.CallTarget != nil {
node["CallTarget"] = callTargetToJSON(e.CallTarget)
}
if e.PropertyName != nil {
node["PropertyName"] = identifierToJSON(e.PropertyName)
}
if e.Collation != nil {
node["Collation"] = identifierToJSON(e.Collation)
}
return node
case *ast.BinaryExpression:
node := jsonNode{
"$type": "BinaryExpression",
Expand Down
199 changes: 162 additions & 37 deletions parser/parse_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,8 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
}
p.nextToken()
return &ast.ParenthesisExpression{Expression: expr}, nil
// Check for property access after parenthesized expression: (c1).SomeProperty
return p.parsePostExpressionAccess(&ast.ParenthesisExpression{Expression: expr})
case TokenCase:
return p.parseCaseExpression()
default:
Expand Down Expand Up @@ -966,58 +967,75 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err
p.nextToken() // consume dot
}

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

// Parse function name
// Parse function/property name
if p.curTok.Type != TokenIdent {
return nil, fmt.Errorf("expected function name after ::, got %s", p.curTok.Literal)
return nil, fmt.Errorf("expected identifier after ::, got %s", p.curTok.Literal)
}
funcName := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
name := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
p.nextToken()

// Expect (
if p.curTok.Type != TokenLParen {
return nil, fmt.Errorf("expected ( after function name, got %s", p.curTok.Literal)
}
p.nextToken() // consume (

// Build SchemaObjectName from identifiers
schemaObjName := identifiersToSchemaObjectName(identifiers)

fc := &ast.FunctionCall{
CallTarget: &ast.UserDefinedTypeCallTarget{
SchemaObjectName: schemaObjName,
},
FunctionName: funcName,
UniqueRowFilter: "NotSpecified",
WithArrayWrapper: false,
}
// If followed by ( it's a method call, otherwise property access
if p.curTok.Type == TokenLParen {
p.nextToken() // consume (

fc := &ast.FunctionCall{
CallTarget: &ast.UserDefinedTypeCallTarget{
SchemaObjectName: schemaObjName,
},
FunctionName: name,
UniqueRowFilter: "NotSpecified",
WithArrayWrapper: false,
}

// Parse parameters
if p.curTok.Type != TokenRParen {
for {
param, err := p.parseScalarExpression()
if err != nil {
return nil, err
}
fc.Parameters = append(fc.Parameters, param)
// Parse parameters
if p.curTok.Type != TokenRParen {
for {
param, err := p.parseScalarExpression()
if err != nil {
return nil, err
}
fc.Parameters = append(fc.Parameters, param)

if p.curTok.Type != TokenComma {
break
if p.curTok.Type != TokenComma {
break
}
p.nextToken() // consume comma
}
p.nextToken() // consume comma
}

// Expect )
if p.curTok.Type != TokenRParen {
return nil, fmt.Errorf("expected ) in function call, got %s", p.curTok.Literal)
}
p.nextToken()

// Check for OVER clause or property access after method call
return p.parsePostExpressionAccess(fc)
}

// Expect )
if p.curTok.Type != TokenRParen {
return nil, fmt.Errorf("expected ) in function call, got %s", p.curTok.Literal)
// Property access: t::a
propAccess := &ast.UserDefinedTypePropertyAccess{
CallTarget: &ast.UserDefinedTypeCallTarget{
SchemaObjectName: schemaObjName,
},
PropertyName: name,
}

// Check for COLLATE clause
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
p.nextToken() // consume COLLATE
propAccess.Collation = p.parseIdentifier()
}
p.nextToken()

return fc, nil
// Check for chained property access
return p.parsePostExpressionAccess(propAccess)
}

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

func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (*ast.FunctionCall, error) {
func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (ast.ScalarExpression, error) {
fc := &ast.FunctionCall{
UniqueRowFilter: "NotSpecified",
WithArrayWrapper: false,
Expand All @@ -1070,6 +1088,15 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier)
// Consume (
p.nextToken()

// Check for ALL or DISTINCT
if strings.ToUpper(p.curTok.Literal) == "ALL" {
fc.UniqueRowFilter = "All"
p.nextToken()
} else if strings.ToUpper(p.curTok.Literal) == "DISTINCT" {
fc.UniqueRowFilter = "Distinct"
p.nextToken()
}

// Parse parameters
if p.curTok.Type != TokenRParen {
for {
Expand All @@ -1092,7 +1119,105 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier)
}
p.nextToken()

return fc, nil
// Check for OVER clause or property access after function call
return p.parsePostExpressionAccess(fc)
}

// parsePostExpressionAccess handles chained property access (.PropertyName), COLLATE clauses, and OVER clauses
// after an expression (function call, parenthesized expression, or property access).
func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.ScalarExpression, error) {
// Loop to handle chained property access like .SomeProperty.AnotherProperty
for {
// Check for .PropertyName pattern (property access)
if p.curTok.Type == TokenDot {
p.nextToken() // consume .

if p.curTok.Type != TokenIdent {
return nil, fmt.Errorf("expected property name after ., got %s", p.curTok.Literal)
}
propName := &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
p.nextToken()

// Check if it's a method call: .method()
if p.curTok.Type == TokenLParen {
p.nextToken() // consume (

fc := &ast.FunctionCall{
CallTarget: &ast.ExpressionCallTarget{
Expression: expr,
},
FunctionName: propName,
UniqueRowFilter: "NotSpecified",
WithArrayWrapper: false,
}

// Parse parameters
if p.curTok.Type != TokenRParen {
for {
param, err := p.parseScalarExpression()
if err != nil {
return nil, err
}
fc.Parameters = append(fc.Parameters, param)

if p.curTok.Type != TokenComma {
break
}
p.nextToken() // consume comma
}
}

// Expect )
if p.curTok.Type != TokenRParen {
return nil, fmt.Errorf("expected ) in method call, got %s", p.curTok.Literal)
}
p.nextToken()

expr = fc
continue
}

// Property access: .PropertyName
propAccess := &ast.UserDefinedTypePropertyAccess{
CallTarget: &ast.ExpressionCallTarget{
Expression: expr,
},
PropertyName: propName,
}

// Check for COLLATE clause
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
p.nextToken() // consume COLLATE
propAccess.Collation = p.parseIdentifier()
}

expr = propAccess
continue
}

// Check for OVER clause for function calls
if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "OVER" {
p.nextToken() // consume OVER

if p.curTok.Type != TokenLParen {
return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal)
}
p.nextToken() // consume (

// For now, just skip to closing paren (basic OVER() support)
// TODO: Parse partition by, order by, and window frame
if p.curTok.Type != TokenRParen {
return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal)
}
p.nextToken() // consume )

fc.OverClause = &ast.OverClause{}
}

break
}

return expr, nil
}

func (p *Parser) parseFromClause() (*ast.FromClause, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
2 changes: 1 addition & 1 deletion parser/testdata/ExpressionTests100/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Loading