@@ -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+
165360func (p * Parser ) parseFunctionParameters () ([]ast.ScalarExpression , error ) {
166361 // Consume (
167362 p .nextToken ()
0 commit comments