Skip to content

Commit 151fc6b

Browse files
authored
Add column list support for OPENROWSET BULK table references (#37)
1 parent 5074d21 commit 151fc6b

8 files changed

Lines changed: 204 additions & 6 deletions

File tree

ast/bulk_insert_statement.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func (o *OrderBulkInsertOption) bulkInsertOption() {}
6767
type BulkOpenRowset struct {
6868
DataFiles []ScalarExpression `json:"DataFiles,omitempty"`
6969
Options []BulkInsertOption `json:"Options,omitempty"`
70+
Columns []*Identifier `json:"Columns,omitempty"`
7071
Alias *Identifier `json:"Alias,omitempty"`
7172
ForPath bool `json:"ForPath"`
7273
}

ast/execute_statement.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ func (e *ExecuteStatement) statement() {}
1010

1111
// ExecuteSpecification contains the details of an EXECUTE.
1212
type ExecuteSpecification struct {
13-
Variable *VariableReference `json:"Variable,omitempty"`
14-
ExecutableEntity ExecutableEntity `json:"ExecutableEntity,omitempty"`
13+
Variable *VariableReference `json:"Variable,omitempty"`
14+
LinkedServer *Identifier `json:"LinkedServer,omitempty"`
15+
ExecuteContext *ExecuteContext `json:"ExecuteContext,omitempty"`
16+
ExecutableEntity ExecutableEntity `json:"ExecutableEntity,omitempty"`
1517
}
1618

1719
// ExecutableEntity is an interface for executable entities.
@@ -27,6 +29,15 @@ type ExecutableProcedureReference struct {
2729

2830
func (e *ExecutableProcedureReference) executableEntity() {}
2931

32+
// ExecutableStringList represents an EXECUTE with a string expression list.
33+
// e.g., EXECUTE ('SELECT * FROM t1', param1, param2)
34+
type ExecutableStringList struct {
35+
Strings []ScalarExpression `json:"Strings,omitempty"`
36+
Parameters []*ExecuteParameter `json:"Parameters,omitempty"`
37+
}
38+
39+
func (e *ExecutableStringList) executableEntity() {}
40+
3041
// ProcedureReferenceName holds either a variable or a procedure reference.
3142
type ProcedureReferenceName struct {
3243
ProcedureVariable *VariableReference `json:"ProcedureVariable,omitempty"`

parser/marshal.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,13 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode {
15301530
}
15311531
node["Options"] = opts
15321532
}
1533+
if len(r.Columns) > 0 {
1534+
cols := make([]jsonNode, len(r.Columns))
1535+
for i, c := range r.Columns {
1536+
cols[i] = identifierToJSON(c)
1537+
}
1538+
node["Columns"] = cols
1539+
}
15331540
if r.Alias != nil {
15341541
node["Alias"] = identifierToJSON(r.Alias)
15351542
}
@@ -1869,6 +1876,12 @@ func executeSpecificationToJSON(spec *ast.ExecuteSpecification) jsonNode {
18691876
if spec.Variable != nil {
18701877
node["Variable"] = scalarExpressionToJSON(spec.Variable)
18711878
}
1879+
if spec.LinkedServer != nil {
1880+
node["LinkedServer"] = identifierToJSON(spec.LinkedServer)
1881+
}
1882+
if spec.ExecuteContext != nil {
1883+
node["ExecuteContext"] = executeContextToJSON(spec.ExecuteContext)
1884+
}
18721885
if spec.ExecutableEntity != nil {
18731886
node["ExecutableEntity"] = executableEntityToJSON(spec.ExecutableEntity)
18741887
}
@@ -1892,6 +1905,25 @@ func executableEntityToJSON(entity ast.ExecutableEntity) jsonNode {
18921905
node["Parameters"] = params
18931906
}
18941907
return node
1908+
case *ast.ExecutableStringList:
1909+
node := jsonNode{
1910+
"$type": "ExecutableStringList",
1911+
}
1912+
if len(e.Strings) > 0 {
1913+
strs := make([]jsonNode, len(e.Strings))
1914+
for i, s := range e.Strings {
1915+
strs[i] = scalarExpressionToJSON(s)
1916+
}
1917+
node["Strings"] = strs
1918+
}
1919+
if len(e.Parameters) > 0 {
1920+
params := make([]jsonNode, len(e.Parameters))
1921+
for i, p := range e.Parameters {
1922+
params[i] = executeParameterToJSON(p)
1923+
}
1924+
node["Parameters"] = params
1925+
}
1926+
return node
18951927
default:
18961928
return jsonNode{"$type": "UnknownExecutableEntity"}
18971929
}

parser/parse_dml.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,25 @@ func (p *Parser) parseBulkOpenRowset() (*ast.BulkOpenRowset, error) {
232232
}
233233
}
234234

235+
// Parse optional column list (e.g., AS a(c1, c2))
236+
if p.curTok.Type == TokenLParen {
237+
p.nextToken()
238+
for {
239+
if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket {
240+
result.Columns = append(result.Columns, p.parseIdentifier())
241+
}
242+
if p.curTok.Type == TokenComma {
243+
p.nextToken()
244+
continue
245+
}
246+
break
247+
}
248+
if p.curTok.Type != TokenRParen {
249+
return nil, fmt.Errorf("expected ) after column list, got %s", p.curTok.Literal)
250+
}
251+
p.nextToken()
252+
}
253+
235254
return result, nil
236255
}
237256

@@ -579,6 +598,32 @@ func (p *Parser) parseExecuteSpecification() (*ast.ExecuteSpecification, error)
579598

580599
spec := &ast.ExecuteSpecification{}
581600

601+
// Check for EXECUTE ('string') form - ExecutableStringList
602+
if p.curTok.Type == TokenLParen {
603+
strList, err := p.parseExecutableStringList()
604+
if err != nil {
605+
return nil, err
606+
}
607+
spec.ExecutableEntity = strList
608+
609+
// Parse optional AS USER/LOGIN context
610+
if p.curTok.Type == TokenAs {
611+
ctx, err := p.parseExecuteContextForSpec()
612+
if err != nil {
613+
return nil, err
614+
}
615+
spec.ExecuteContext = ctx
616+
}
617+
618+
// Parse optional AT LinkedServer
619+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "AT" {
620+
p.nextToken()
621+
spec.LinkedServer = p.parseIdentifier()
622+
}
623+
624+
return spec, nil
625+
}
626+
582627
// Check for return variable assignment @var =
583628
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
584629
varName := p.curTok.Literal
@@ -636,6 +681,115 @@ func (p *Parser) parseExecuteSpecification() (*ast.ExecuteSpecification, error)
636681
return spec, nil
637682
}
638683

684+
func (p *Parser) parseExecutableStringList() (*ast.ExecutableStringList, error) {
685+
// We're positioned on (, consume it
686+
p.nextToken()
687+
688+
strList := &ast.ExecutableStringList{}
689+
690+
// Parse the first string expression (may be concatenated with +)
691+
for {
692+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
693+
expr, err := p.parseScalarExpression()
694+
if err != nil {
695+
return nil, err
696+
}
697+
// parseScalarExpression handles the + concatenation, so we get a BinaryExpression
698+
// But we need to flatten it to individual StringLiterals for the Strings array
699+
p.flattenStringExpression(expr, &strList.Strings)
700+
} else {
701+
break
702+
}
703+
704+
// Check for comma (parameters follow) or closing paren
705+
if p.curTok.Type == TokenComma {
706+
p.nextToken()
707+
break
708+
}
709+
if p.curTok.Type == TokenRParen {
710+
break
711+
}
712+
}
713+
714+
// Parse parameters (after the first comma)
715+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
716+
param, err := p.parseExecuteParameter()
717+
if err != nil {
718+
return nil, err
719+
}
720+
strList.Parameters = append(strList.Parameters, param)
721+
722+
if p.curTok.Type != TokenComma {
723+
break
724+
}
725+
p.nextToken()
726+
}
727+
728+
if p.curTok.Type != TokenRParen {
729+
return nil, fmt.Errorf("expected ) after EXECUTE string list, got %s", p.curTok.Literal)
730+
}
731+
p.nextToken()
732+
733+
return strList, nil
734+
}
735+
736+
func (p *Parser) flattenStringExpression(expr ast.ScalarExpression, strings *[]ast.ScalarExpression) {
737+
switch e := expr.(type) {
738+
case *ast.BinaryExpression:
739+
// Recursively flatten for + concatenation
740+
p.flattenStringExpression(e.FirstExpression, strings)
741+
p.flattenStringExpression(e.SecondExpression, strings)
742+
default:
743+
*strings = append(*strings, expr)
744+
}
745+
}
746+
747+
func (p *Parser) parseExecuteContextForSpec() (*ast.ExecuteContext, error) {
748+
// We're positioned on AS, consume it
749+
p.nextToken()
750+
751+
ctx := &ast.ExecuteContext{}
752+
753+
upper := strings.ToUpper(p.curTok.Literal)
754+
switch upper {
755+
case "USER":
756+
ctx.Kind = "User"
757+
p.nextToken()
758+
if p.curTok.Type == TokenEquals {
759+
p.nextToken()
760+
expr, err := p.parseScalarExpression()
761+
if err != nil {
762+
return nil, err
763+
}
764+
ctx.Principal = expr
765+
}
766+
case "LOGIN":
767+
ctx.Kind = "Login"
768+
p.nextToken()
769+
if p.curTok.Type == TokenEquals {
770+
p.nextToken()
771+
expr, err := p.parseScalarExpression()
772+
if err != nil {
773+
return nil, err
774+
}
775+
ctx.Principal = expr
776+
}
777+
case "CALLER":
778+
ctx.Kind = "Caller"
779+
p.nextToken()
780+
case "OWNER":
781+
ctx.Kind = "Owner"
782+
p.nextToken()
783+
case "SELF":
784+
ctx.Kind = "Self"
785+
p.nextToken()
786+
default:
787+
return nil, fmt.Errorf("expected USER, LOGIN, CALLER, OWNER, or SELF after AS, got %s", p.curTok.Literal)
788+
}
789+
790+
return ctx, nil
791+
}
792+
639793
func (p *Parser) parseExecuteParameter() (*ast.ExecuteParameter, error) {
640794
param := &ast.ExecuteParameter{IsOutput: false}
641795

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+
{}
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)