Skip to content

Commit 021f974

Browse files
committed
Add comprehensive EXECUTE statement parsing support
- Add named parameters (@name = value) support - Add DEFAULT keyword as parameter value - Add OUTPUT modifier on parameters - Add procedure numbers (;1 suffix) parsing - Add OPENDATASOURCE/OPENROWSET ad-hoc data source support - Add IdentifierLiteral for bare identifier parameters - Fix DefaultLiteral to include LiteralType field - Add AdHocDataSource AST type and JSON marshaling Enables BaselinesCommon_ExecuteStatementTests
1 parent 7bcebdf commit 021f974

4 files changed

Lines changed: 166 additions & 10 deletions

File tree

ast/execute_statement.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type ExecutableEntity interface {
8181
type ExecutableProcedureReference struct {
8282
ProcedureReference *ProcedureReferenceName `json:"ProcedureReference,omitempty"`
8383
Parameters []*ExecuteParameter `json:"Parameters,omitempty"`
84+
AdHocDataSource *AdHocDataSource `json:"AdHocDataSource,omitempty"`
8485
}
8586

8687
func (e *ExecutableProcedureReference) executableEntity() {}
@@ -102,12 +103,19 @@ type ProcedureReferenceName struct {
102103

103104
// ProcedureReference references a stored procedure by name.
104105
type ProcedureReference struct {
105-
Name *SchemaObjectName `json:"Name,omitempty"`
106+
Name *SchemaObjectName `json:"Name,omitempty"`
107+
Number *IntegerLiteral `json:"Number,omitempty"`
106108
}
107109

108110
// ExecuteParameter represents a parameter to an EXEC call.
109111
type ExecuteParameter struct {
110-
ParameterValue ScalarExpression `json:"ParameterValue,omitempty"`
112+
ParameterValue ScalarExpression `json:"ParameterValue,omitempty"`
111113
Variable *VariableReference `json:"Variable,omitempty"`
112-
IsOutput bool `json:"IsOutput"`
114+
IsOutput bool `json:"IsOutput"`
115+
}
116+
117+
// AdHocDataSource represents an OPENDATASOURCE or OPENROWSET call for ad-hoc data access.
118+
type AdHocDataSource struct {
119+
ProviderName *StringLiteral `json:"ProviderName,omitempty"`
120+
InitString *StringLiteral `json:"InitString,omitempty"`
113121
}

parser/marshal.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,9 @@ func executableEntityToJSON(entity ast.ExecutableEntity) jsonNode {
24062406
}
24072407
node["Parameters"] = params
24082408
}
2409+
if e.AdHocDataSource != nil {
2410+
node["AdHocDataSource"] = adHocDataSourceToJSON(e.AdHocDataSource)
2411+
}
24092412
return node
24102413
case *ast.ExecutableStringList:
24112414
node := jsonNode{
@@ -2451,6 +2454,9 @@ func procedureReferenceToJSON(pr *ast.ProcedureReference) jsonNode {
24512454
if pr.Name != nil {
24522455
node["Name"] = schemaObjectNameToJSON(pr.Name)
24532456
}
2457+
if pr.Number != nil {
2458+
node["Number"] = scalarExpressionToJSON(pr.Number)
2459+
}
24542460
return node
24552461
}
24562462

@@ -2468,6 +2474,19 @@ func executeParameterToJSON(ep *ast.ExecuteParameter) jsonNode {
24682474
return node
24692475
}
24702476

2477+
func adHocDataSourceToJSON(ds *ast.AdHocDataSource) jsonNode {
2478+
node := jsonNode{
2479+
"$type": "AdHocDataSource",
2480+
}
2481+
if ds.ProviderName != nil {
2482+
node["ProviderName"] = scalarExpressionToJSON(ds.ProviderName)
2483+
}
2484+
if ds.InitString != nil {
2485+
node["InitString"] = scalarExpressionToJSON(ds.InitString)
2486+
}
2487+
return node
2488+
}
2489+
24712490
func updateStatementToJSON(s *ast.UpdateStatement) jsonNode {
24722491
node := jsonNode{
24732492
"$type": "UpdateStatement",

parser/parse_dml.go

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -865,20 +865,77 @@ func (p *Parser) parseExecuteSpecification() (*ast.ExecuteSpecification, error)
865865
// Parse procedure reference
866866
procRef := &ast.ExecutableProcedureReference{}
867867

868+
// Check for OPENDATASOURCE or OPENROWSET
869+
upperLit := strings.ToUpper(p.curTok.Literal)
870+
if upperLit == "OPENDATASOURCE" || upperLit == "OPENROWSET" {
871+
p.nextToken() // consume OPENDATASOURCE/OPENROWSET
872+
if p.curTok.Type == TokenLParen {
873+
p.nextToken() // consume (
874+
875+
// Parse provider name
876+
var providerName *ast.StringLiteral
877+
if p.curTok.Type == TokenString {
878+
providerName = p.parseStringLiteralValue()
879+
p.nextToken()
880+
}
881+
882+
// Expect comma
883+
if p.curTok.Type == TokenComma {
884+
p.nextToken()
885+
}
886+
887+
// Parse init string
888+
var initString *ast.StringLiteral
889+
if p.curTok.Type == TokenString {
890+
initString = p.parseStringLiteralValue()
891+
p.nextToken()
892+
}
893+
894+
// Expect )
895+
if p.curTok.Type == TokenRParen {
896+
p.nextToken()
897+
}
898+
899+
procRef.AdHocDataSource = &ast.AdHocDataSource{
900+
ProviderName: providerName,
901+
InitString: initString,
902+
}
903+
904+
// Expect . and then schema.object.procedure name
905+
if p.curTok.Type == TokenDot {
906+
p.nextToken() // consume .
907+
}
908+
}
909+
}
910+
868911
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
869912
// Procedure variable
870913
procRef.ProcedureReference = &ast.ProcedureReferenceName{
871914
ProcedureVariable: &ast.VariableReference{Name: p.curTok.Literal},
872915
}
873916
p.nextToken()
874-
} else {
917+
} else if p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon {
875918
// Procedure name
876919
son, err := p.parseSchemaObjectName()
877920
if err != nil {
878921
return nil, err
879922
}
923+
pr := &ast.ProcedureReference{Name: son}
924+
925+
// Check for procedure number: ;number
926+
if p.curTok.Type == TokenSemicolon {
927+
p.nextToken() // consume ;
928+
if p.curTok.Type == TokenNumber {
929+
pr.Number = &ast.IntegerLiteral{
930+
LiteralType: "Integer",
931+
Value: p.curTok.Literal,
932+
}
933+
p.nextToken()
934+
}
935+
}
936+
880937
procRef.ProcedureReference = &ast.ProcedureReferenceName{
881-
ProcedureReference: &ast.ProcedureReference{Name: son},
938+
ProcedureReference: pr,
882939
}
883940
}
884941

@@ -1015,11 +1072,83 @@ func (p *Parser) parseExecuteContextForSpec() (*ast.ExecuteContext, error) {
10151072
func (p *Parser) parseExecuteParameter() (*ast.ExecuteParameter, error) {
10161073
param := &ast.ExecuteParameter{IsOutput: false}
10171074

1018-
expr, err := p.parseScalarExpression()
1019-
if err != nil {
1020-
return nil, err
1075+
// Check for DEFAULT keyword
1076+
if strings.ToUpper(p.curTok.Literal) == "DEFAULT" {
1077+
param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"}
1078+
p.nextToken()
1079+
return param, nil
1080+
}
1081+
1082+
// Check for named parameter: @name = value
1083+
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
1084+
varName := p.curTok.Literal
1085+
p.nextToken()
1086+
1087+
if p.curTok.Type == TokenEquals {
1088+
// Named parameter
1089+
p.nextToken() // consume =
1090+
param.Variable = &ast.VariableReference{Name: varName}
1091+
1092+
// Check for DEFAULT keyword as value
1093+
if strings.ToUpper(p.curTok.Literal) == "DEFAULT" {
1094+
param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"}
1095+
p.nextToken()
1096+
} else {
1097+
// Parse the parameter value
1098+
expr, err := p.parseScalarExpression()
1099+
if err != nil {
1100+
return nil, err
1101+
}
1102+
param.ParameterValue = expr
1103+
}
1104+
} else {
1105+
// Just a variable as value (not a named parameter)
1106+
param.ParameterValue = &ast.VariableReference{Name: varName}
1107+
}
1108+
} else {
1109+
// Check for bare identifier as IdentifierLiteral (e.g., EXEC sp_addtype birthday, datetime)
1110+
// Only if it's not followed by . or ( which would indicate a column/function reference
1111+
if p.curTok.Type == TokenIdent && !strings.HasPrefix(p.curTok.Literal, "@") {
1112+
upper := strings.ToUpper(p.curTok.Literal)
1113+
// Skip keywords that are expression starters
1114+
isKeyword := upper == "NULL" || upper == "DEFAULT" || upper == "NOT" ||
1115+
upper == "CASE" || upper == "EXISTS" || upper == "CAST" ||
1116+
upper == "CONVERT" || upper == "COALESCE" || upper == "NULLIF"
1117+
if !isKeyword && p.peekTok.Type != TokenDot && p.peekTok.Type != TokenLParen {
1118+
// Plain identifier - treat as IdentifierLiteral
1119+
quoteType := "NotQuoted"
1120+
if strings.HasPrefix(p.curTok.Literal, "[") {
1121+
quoteType = "SquareBracket"
1122+
}
1123+
param.ParameterValue = &ast.IdentifierLiteral{
1124+
LiteralType: "Identifier",
1125+
QuoteType: quoteType,
1126+
Value: p.curTok.Literal,
1127+
}
1128+
p.nextToken()
1129+
} else {
1130+
// Regular value expression
1131+
expr, err := p.parseScalarExpression()
1132+
if err != nil {
1133+
return nil, err
1134+
}
1135+
param.ParameterValue = expr
1136+
}
1137+
} else {
1138+
// Regular value expression
1139+
expr, err := p.parseScalarExpression()
1140+
if err != nil {
1141+
return nil, err
1142+
}
1143+
param.ParameterValue = expr
1144+
}
1145+
}
1146+
1147+
// Check for OUTPUT modifier
1148+
if strings.ToUpper(p.curTok.Literal) == "OUTPUT" || strings.ToUpper(p.curTok.Literal) == "OUT" {
1149+
param.IsOutput = true
1150+
p.nextToken()
10211151
}
1022-
param.ParameterValue = expr
10231152

10241153
return param, nil
10251154
}
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)