Skip to content

Commit 4a70ce5

Browse files
kyleconroyclaude
andauthored
Add OPENROWSET BULK support and enable 2 more tests (#29)
- Add BulkOpenRowset AST type for OPENROWSET(BULK ...) syntax - Add parser support for OPENROWSET BULK with ORDER and UNIQUE options - Add JSON marshaling for BulkOpenRowset - Always include IsUnique field in OrderBulkInsertOption JSON output - Enable Baselines100_RowsetsInSelectTests100 and RowsetsInSelectTests100 - Update skipped_tests_by_size.txt Co-authored-by: Claude <[email protected]>
1 parent aaa254d commit 4a70ce5

File tree

7 files changed

+321
-13
lines changed

7 files changed

+321
-13
lines changed

ast/bulk_insert_statement.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,14 @@ type OrderBulkInsertOption struct {
6262
func (o *OrderBulkInsertOption) bulkInsertOption() {}
6363

6464
// Note: ColumnWithSortOrder is defined in create_table_statement.go
65+
66+
// BulkOpenRowset represents an OPENROWSET (BULK ...) table reference.
67+
type BulkOpenRowset struct {
68+
DataFiles []ScalarExpression `json:"DataFiles,omitempty"`
69+
Options []BulkInsertOption `json:"Options,omitempty"`
70+
Alias *Identifier `json:"Alias,omitempty"`
71+
ForPath bool `json:"ForPath"`
72+
}
73+
74+
func (b *BulkOpenRowset) node() {}
75+
func (b *BulkOpenRowset) tableReference() {}

parser/marshal.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,29 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode {
15101510
}
15111511
node["ForPath"] = r.ForPath
15121512
return node
1513+
case *ast.BulkOpenRowset:
1514+
node := jsonNode{
1515+
"$type": "BulkOpenRowset",
1516+
}
1517+
if len(r.DataFiles) > 0 {
1518+
files := make([]jsonNode, len(r.DataFiles))
1519+
for i, f := range r.DataFiles {
1520+
files[i] = scalarExpressionToJSON(f)
1521+
}
1522+
node["DataFiles"] = files
1523+
}
1524+
if len(r.Options) > 0 {
1525+
opts := make([]jsonNode, len(r.Options))
1526+
for i, o := range r.Options {
1527+
opts[i] = bulkInsertOptionToJSON(o)
1528+
}
1529+
node["Options"] = opts
1530+
}
1531+
if r.Alias != nil {
1532+
node["Alias"] = identifierToJSON(r.Alias)
1533+
}
1534+
node["ForPath"] = r.ForPath
1535+
return node
15131536
default:
15141537
return jsonNode{"$type": "UnknownTableReference"}
15151538
}
@@ -6157,6 +6180,7 @@ func bulkInsertOptionToJSON(opt ast.BulkInsertOption) jsonNode {
61576180
node := jsonNode{
61586181
"$type": "OrderBulkInsertOption",
61596182
"OptionKind": "Order",
6183+
"IsUnique": o.IsUnique,
61606184
}
61616185
if len(o.Columns) > 0 {
61626186
cols := make([]jsonNode, len(o.Columns))
@@ -6165,9 +6189,6 @@ func bulkInsertOptionToJSON(opt ast.BulkInsertOption) jsonNode {
61656189
}
61666190
node["Columns"] = cols
61676191
}
6168-
if o.IsUnique {
6169-
node["IsUnique"] = o.IsUnique
6170-
}
61716192
return node
61726193
default:
61736194
return jsonNode{"$type": "UnknownBulkInsertOption"}

parser/parse_dml.go

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func (p *Parser) parseDMLTarget() (ast.TableReference, error) {
124124
return ref, nil
125125
}
126126

127-
func (p *Parser) parseOpenRowset() (*ast.InternalOpenRowset, error) {
127+
func (p *Parser) parseOpenRowset() (ast.TableReference, error) {
128128
// Consume OPENROWSET
129129
p.nextToken()
130130

@@ -133,6 +133,11 @@ func (p *Parser) parseOpenRowset() (*ast.InternalOpenRowset, error) {
133133
}
134134
p.nextToken()
135135

136+
// Check for BULK form
137+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "BULK" {
138+
return p.parseBulkOpenRowset()
139+
}
140+
136141
// Parse identifier
137142
if p.curTok.Type != TokenIdent {
138143
return nil, fmt.Errorf("expected identifier in OPENROWSET, got %s", p.curTok.Literal)
@@ -162,6 +167,196 @@ func (p *Parser) parseOpenRowset() (*ast.InternalOpenRowset, error) {
162167
}, nil
163168
}
164169

170+
func (p *Parser) parseBulkOpenRowset() (*ast.BulkOpenRowset, error) {
171+
// We're positioned on BULK, consume it
172+
p.nextToken()
173+
174+
result := &ast.BulkOpenRowset{
175+
ForPath: false,
176+
}
177+
178+
// Parse data file(s) - could be a single string or parenthesized list
179+
if p.curTok.Type == TokenLParen {
180+
// Multiple data files
181+
p.nextToken()
182+
for {
183+
expr, err := p.parseScalarExpression()
184+
if err != nil {
185+
return nil, err
186+
}
187+
result.DataFiles = append(result.DataFiles, expr)
188+
189+
if p.curTok.Type == TokenComma {
190+
p.nextToken()
191+
// Allow trailing comma
192+
if p.curTok.Type == TokenRParen {
193+
break
194+
}
195+
continue
196+
}
197+
break
198+
}
199+
if p.curTok.Type != TokenRParen {
200+
return nil, fmt.Errorf("expected ) after data files, got %s", p.curTok.Literal)
201+
}
202+
p.nextToken()
203+
} else {
204+
// Single data file
205+
expr, err := p.parseScalarExpression()
206+
if err != nil {
207+
return nil, err
208+
}
209+
result.DataFiles = append(result.DataFiles, expr)
210+
}
211+
212+
// Parse options (comma-separated)
213+
for p.curTok.Type == TokenComma {
214+
p.nextToken()
215+
opt, err := p.parseOpenRowsetBulkOption()
216+
if err != nil {
217+
return nil, err
218+
}
219+
result.Options = append(result.Options, opt)
220+
}
221+
222+
if p.curTok.Type != TokenRParen {
223+
return nil, fmt.Errorf("expected ) after OPENROWSET BULK, got %s", p.curTok.Literal)
224+
}
225+
p.nextToken()
226+
227+
// Parse optional alias
228+
if p.curTok.Type == TokenAs {
229+
p.nextToken()
230+
if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket {
231+
result.Alias = p.parseIdentifier()
232+
}
233+
}
234+
235+
return result, nil
236+
}
237+
238+
func (p *Parser) parseOpenRowsetBulkOption() (ast.BulkInsertOption, error) {
239+
upper := strings.ToUpper(p.curTok.Literal)
240+
241+
// Handle simple options (SINGLE_BLOB, SINGLE_CLOB, SINGLE_NCLOB)
242+
switch upper {
243+
case "SINGLE_BLOB":
244+
p.nextToken()
245+
return &ast.BulkInsertOptionBase{OptionKind: "SingleBlob"}, nil
246+
case "SINGLE_CLOB":
247+
p.nextToken()
248+
return &ast.BulkInsertOptionBase{OptionKind: "SingleClob"}, nil
249+
case "SINGLE_NCLOB":
250+
p.nextToken()
251+
return &ast.BulkInsertOptionBase{OptionKind: "SingleNClob"}, nil
252+
}
253+
254+
// Handle ORDER option
255+
if upper == "ORDER" {
256+
p.nextToken()
257+
return p.parseOpenRowsetOrderOption()
258+
}
259+
260+
// Handle KEY=VALUE options
261+
optionKind := p.getOpenRowsetOptionKind(upper)
262+
p.nextToken()
263+
264+
if p.curTok.Type == TokenEquals {
265+
p.nextToken()
266+
value, err := p.parseScalarExpression()
267+
if err != nil {
268+
return nil, err
269+
}
270+
return &ast.LiteralBulkInsertOption{
271+
OptionKind: optionKind,
272+
Value: value,
273+
}, nil
274+
}
275+
276+
return &ast.BulkInsertOptionBase{OptionKind: optionKind}, nil
277+
}
278+
279+
func (p *Parser) getOpenRowsetOptionKind(name string) string {
280+
optionMap := map[string]string{
281+
"FORMATFILE": "FormatFile",
282+
"FORMAT": "Format",
283+
"CODEPAGE": "CodePage",
284+
"ROWS_PER_BATCH": "RowsPerBatch",
285+
"LASTROW": "LastRow",
286+
"FIRSTROW": "FirstRow",
287+
"MAXERRORS": "MaxErrors",
288+
"ERRORFILE": "ErrorFile",
289+
"FIELDQUOTE": "FieldQuote",
290+
"FIELDTERMINATOR": "FieldTerminator",
291+
"ROWTERMINATOR": "RowTerminator",
292+
"ESCAPECHAR": "EscapeChar",
293+
"DATA_COMPRESSION": "DataCompression",
294+
"PARSER_VERSION": "ParserVersion",
295+
"HEADER_ROW": "HeaderRow",
296+
"DATAFILETYPE": "DataFileType",
297+
"ROWSET_OPTIONS": "RowsetOptions",
298+
}
299+
if kind, ok := optionMap[name]; ok {
300+
return kind
301+
}
302+
return name
303+
}
304+
305+
func (p *Parser) parseOpenRowsetOrderOption() (*ast.OrderBulkInsertOption, error) {
306+
result := &ast.OrderBulkInsertOption{
307+
OptionKind: "Order",
308+
}
309+
310+
if p.curTok.Type != TokenLParen {
311+
return nil, fmt.Errorf("expected ( after ORDER, got %s", p.curTok.Literal)
312+
}
313+
p.nextToken()
314+
315+
// Parse column list with sort order
316+
for {
317+
col := &ast.ColumnWithSortOrder{
318+
SortOrder: ast.SortOrderNotSpecified,
319+
}
320+
321+
// Parse column reference
322+
colRef, err := p.parseMultiPartIdentifierAsColumn()
323+
if err != nil {
324+
return nil, err
325+
}
326+
col.Column = colRef
327+
328+
// Check for ASC/DESC
329+
if p.curTok.Type == TokenAsc {
330+
col.SortOrder = ast.SortOrderAscending
331+
p.nextToken()
332+
} else if p.curTok.Type == TokenDesc {
333+
col.SortOrder = ast.SortOrderDescending
334+
p.nextToken()
335+
}
336+
337+
result.Columns = append(result.Columns, col)
338+
339+
if p.curTok.Type == TokenComma {
340+
p.nextToken()
341+
continue
342+
}
343+
break
344+
}
345+
346+
if p.curTok.Type != TokenRParen {
347+
return nil, fmt.Errorf("expected ) after ORDER columns, got %s", p.curTok.Literal)
348+
}
349+
p.nextToken()
350+
351+
// Check for UNIQUE
352+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "UNIQUE" {
353+
result.IsUnique = true
354+
p.nextToken()
355+
}
356+
357+
return result, nil
358+
}
359+
165360
func (p *Parser) parseFunctionParameters() ([]ast.ScalarExpression, error) {
166361
// Consume (
167362
p.nextToken()

parser/parse_select.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ func (p *Parser) parseFromClause() (*ast.FromClause, error) {
10701070

10711071
func (p *Parser) parseTableReference() (ast.TableReference, error) {
10721072
// Parse the base table reference
1073-
baseRef, err := p.parseNamedTableReference()
1073+
baseRef, err := p.parseSingleTableReference()
10741074
if err != nil {
10751075
return nil, err
10761076
}
@@ -1086,7 +1086,7 @@ func (p *Parser) parseTableReference() (ast.TableReference, error) {
10861086
}
10871087
p.nextToken() // consume JOIN
10881088

1089-
right, err := p.parseNamedTableReference()
1089+
right, err := p.parseSingleTableReference()
10901090
if err != nil {
10911091
return nil, err
10921092
}
@@ -1135,7 +1135,7 @@ func (p *Parser) parseTableReference() (ast.TableReference, error) {
11351135
}
11361136
p.nextToken() // consume JOIN
11371137

1138-
right, err := p.parseNamedTableReference()
1138+
right, err := p.parseSingleTableReference()
11391139
if err != nil {
11401140
return nil, err
11411141
}
@@ -1162,6 +1162,25 @@ func (p *Parser) parseTableReference() (ast.TableReference, error) {
11621162
return left, nil
11631163
}
11641164

1165+
func (p *Parser) parseSingleTableReference() (ast.TableReference, error) {
1166+
// Check for OPENROWSET
1167+
if p.curTok.Type == TokenOpenRowset {
1168+
return p.parseOpenRowset()
1169+
}
1170+
1171+
// Check for variable table reference
1172+
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
1173+
name := p.curTok.Literal
1174+
p.nextToken()
1175+
return &ast.VariableTableReference{
1176+
Variable: &ast.VariableReference{Name: name},
1177+
ForPath: false,
1178+
}, nil
1179+
}
1180+
1181+
return p.parseNamedTableReference()
1182+
}
1183+
11651184
func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) {
11661185
ref := &ast.NamedTableReference{
11671186
ForPath: false,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"skip": true}
1+
{"skip": false}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"skip": true}
1+
{"skip": false}

0 commit comments

Comments
 (0)