Skip to content

Commit 50719c9

Browse files
committed
Add FOR clause parsing support for SELECT statements
- Add ForClause interface and types: BrowseForClause, ReadOnlyForClause, UpdateForClause, XmlForClause, XmlForClauseOption - Add ForClause field to QuerySpecification - Parse FOR BROWSE, FOR READ ONLY, FOR UPDATE [OF columns] clauses - Parse FOR XML with options: AUTO, EXPLICIT, RAW, PATH, ELEMENTS, XMLDATA, XMLSCHEMA, ROOT, TYPE, BINARY BASE64 - Add "FOR" to reserved keywords that shouldn't be parsed as table aliases - Add JSON marshaling for all FOR clause types Enables BaselinesCommon_ForClauseTests and ForClauseTests tests.
1 parent 15a628f commit 50719c9

File tree

6 files changed

+270
-4
lines changed

6 files changed

+270
-4
lines changed

ast/for_clause.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ast
2+
3+
// ForClause is an interface for different types of FOR clauses.
4+
type ForClause interface {
5+
Node
6+
forClause()
7+
}
8+
9+
// BrowseForClause represents a FOR BROWSE clause.
10+
type BrowseForClause struct{}
11+
12+
func (*BrowseForClause) node() {}
13+
func (*BrowseForClause) forClause() {}
14+
15+
// ReadOnlyForClause represents a FOR READ ONLY clause.
16+
type ReadOnlyForClause struct{}
17+
18+
func (*ReadOnlyForClause) node() {}
19+
func (*ReadOnlyForClause) forClause() {}
20+
21+
// UpdateForClause represents a FOR UPDATE [OF columns] clause.
22+
type UpdateForClause struct {
23+
Columns []*ColumnReferenceExpression `json:"Columns,omitempty"`
24+
}
25+
26+
func (*UpdateForClause) node() {}
27+
func (*UpdateForClause) forClause() {}
28+
29+
// XmlForClause represents a FOR XML clause with its options.
30+
type XmlForClause struct {
31+
Options []*XmlForClauseOption `json:"Options,omitempty"`
32+
}
33+
34+
func (*XmlForClause) node() {}
35+
func (*XmlForClause) forClause() {}
36+
37+
// XmlForClauseOption represents an option in a FOR XML clause.
38+
type XmlForClauseOption struct {
39+
OptionKind string `json:"OptionKind,omitempty"`
40+
Value *StringLiteral `json:"Value,omitempty"`
41+
}
42+
43+
func (*XmlForClauseOption) node() {}

ast/query_specification.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type QuerySpecification struct {
1010
GroupByClause *GroupByClause `json:"GroupByClause,omitempty"`
1111
HavingClause *HavingClause `json:"HavingClause,omitempty"`
1212
OrderByClause *OrderByClause `json:"OrderByClause,omitempty"`
13+
ForClause ForClause `json:"ForClause,omitempty"`
1314
}
1415

1516
func (*QuerySpecification) node() {}

parser/marshal.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,51 @@ func querySpecificationToJSON(q *ast.QuerySpecification) jsonNode {
12531253
if q.OrderByClause != nil {
12541254
node["OrderByClause"] = orderByClauseToJSON(q.OrderByClause)
12551255
}
1256+
if q.ForClause != nil {
1257+
node["ForClause"] = forClauseToJSON(q.ForClause)
1258+
}
1259+
return node
1260+
}
1261+
1262+
func forClauseToJSON(fc ast.ForClause) jsonNode {
1263+
switch f := fc.(type) {
1264+
case *ast.BrowseForClause:
1265+
return jsonNode{"$type": "BrowseForClause"}
1266+
case *ast.ReadOnlyForClause:
1267+
return jsonNode{"$type": "ReadOnlyForClause"}
1268+
case *ast.UpdateForClause:
1269+
node := jsonNode{"$type": "UpdateForClause"}
1270+
if len(f.Columns) > 0 {
1271+
cols := make([]jsonNode, len(f.Columns))
1272+
for i, col := range f.Columns {
1273+
cols[i] = columnReferenceExpressionToJSON(col)
1274+
}
1275+
node["Columns"] = cols
1276+
}
1277+
return node
1278+
case *ast.XmlForClause:
1279+
node := jsonNode{"$type": "XmlForClause"}
1280+
if len(f.Options) > 0 {
1281+
opts := make([]jsonNode, len(f.Options))
1282+
for i, opt := range f.Options {
1283+
opts[i] = xmlForClauseOptionToJSON(opt)
1284+
}
1285+
node["Options"] = opts
1286+
}
1287+
return node
1288+
default:
1289+
return jsonNode{"$type": "UnknownForClause"}
1290+
}
1291+
}
1292+
1293+
func xmlForClauseOptionToJSON(opt *ast.XmlForClauseOption) jsonNode {
1294+
node := jsonNode{"$type": "XmlForClauseOption"}
1295+
if opt.OptionKind != "" {
1296+
node["OptionKind"] = opt.OptionKind
1297+
}
1298+
if opt.Value != nil {
1299+
node["Value"] = stringLiteralToJSON(opt.Value)
1300+
}
12561301
return node
12571302
}
12581303

parser/parse_select.go

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ func (p *Parser) parseQueryExpressionWithInto() (ast.QueryExpression, *ast.Schem
186186
}
187187
}
188188

189+
// Parse FOR clause (FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY)
190+
if strings.ToUpper(p.curTok.Literal) == "FOR" {
191+
forClause, err := p.parseForClause()
192+
if err != nil {
193+
return nil, nil, nil, err
194+
}
195+
// Attach to QuerySpecification
196+
if qs, ok := left.(*ast.QuerySpecification); ok {
197+
qs.ForClause = forClause
198+
}
199+
}
200+
189201
return left, into, on, nil
190202
}
191203

@@ -1797,7 +1809,7 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) {
17971809
} else if p.curTok.Type == TokenIdent {
17981810
// Could be an alias without AS, but need to be careful not to consume keywords
17991811
upper := strings.ToUpper(p.curTok.Literal)
1800-
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" {
1812+
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" {
18011813
ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
18021814
p.nextToken()
18031815
}
@@ -1857,7 +1869,7 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
18571869
// Could be an alias without AS, but need to be careful not to consume keywords
18581870
if p.curTok.Type == TokenIdent {
18591871
upper := strings.ToUpper(p.curTok.Literal)
1860-
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" {
1872+
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" {
18611873
ref.Alias = p.parseIdentifier()
18621874
}
18631875
} else {
@@ -3436,3 +3448,168 @@ func (p *Parser) parsePredictTableReference() (*ast.PredictTableReference, error
34363448

34373449
return ref, nil
34383450
}
3451+
3452+
// parseForClause parses FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY clauses.
3453+
func (p *Parser) parseForClause() (ast.ForClause, error) {
3454+
p.nextToken() // consume FOR
3455+
3456+
keyword := strings.ToUpper(p.curTok.Literal)
3457+
3458+
switch keyword {
3459+
case "BROWSE":
3460+
p.nextToken() // consume BROWSE
3461+
return &ast.BrowseForClause{}, nil
3462+
3463+
case "READ":
3464+
p.nextToken() // consume READ
3465+
if strings.ToUpper(p.curTok.Literal) == "ONLY" {
3466+
p.nextToken() // consume ONLY
3467+
}
3468+
return &ast.ReadOnlyForClause{}, nil
3469+
3470+
case "UPDATE":
3471+
p.nextToken() // consume UPDATE
3472+
clause := &ast.UpdateForClause{}
3473+
3474+
// Check for OF column_list
3475+
if strings.ToUpper(p.curTok.Literal) == "OF" {
3476+
p.nextToken() // consume OF
3477+
3478+
// Parse column list
3479+
for {
3480+
col, err := p.parseColumnReference()
3481+
if err != nil {
3482+
return nil, err
3483+
}
3484+
clause.Columns = append(clause.Columns, col)
3485+
3486+
if p.curTok.Type != TokenComma {
3487+
break
3488+
}
3489+
p.nextToken() // consume comma
3490+
}
3491+
}
3492+
return clause, nil
3493+
3494+
case "XML":
3495+
p.nextToken() // consume XML
3496+
return p.parseXmlForClause()
3497+
3498+
default:
3499+
return nil, fmt.Errorf("unexpected token after FOR: %s", p.curTok.Literal)
3500+
}
3501+
}
3502+
3503+
// parseXmlForClause parses FOR XML options.
3504+
func (p *Parser) parseXmlForClause() (*ast.XmlForClause, error) {
3505+
clause := &ast.XmlForClause{}
3506+
3507+
// Parse XML options separated by commas
3508+
for {
3509+
option, err := p.parseXmlForClauseOption()
3510+
if err != nil {
3511+
return nil, err
3512+
}
3513+
clause.Options = append(clause.Options, option)
3514+
3515+
if p.curTok.Type != TokenComma {
3516+
break
3517+
}
3518+
p.nextToken() // consume comma
3519+
}
3520+
3521+
return clause, nil
3522+
}
3523+
3524+
// parseXmlForClauseOption parses a single XML FOR clause option.
3525+
func (p *Parser) parseXmlForClauseOption() (*ast.XmlForClauseOption, error) {
3526+
option := &ast.XmlForClauseOption{}
3527+
3528+
keyword := strings.ToUpper(p.curTok.Literal)
3529+
p.nextToken() // consume the option keyword
3530+
3531+
switch keyword {
3532+
case "AUTO":
3533+
option.OptionKind = "Auto"
3534+
case "EXPLICIT":
3535+
option.OptionKind = "Explicit"
3536+
case "RAW":
3537+
option.OptionKind = "Raw"
3538+
// Check for optional element name: RAW ('name')
3539+
if p.curTok.Type == TokenLParen {
3540+
p.nextToken() // consume (
3541+
if p.curTok.Type == TokenString {
3542+
option.Value = p.parseStringLiteralValue()
3543+
p.nextToken() // consume string
3544+
}
3545+
if p.curTok.Type == TokenRParen {
3546+
p.nextToken() // consume )
3547+
}
3548+
}
3549+
case "PATH":
3550+
option.OptionKind = "Path"
3551+
// Check for optional path name: PATH ('name')
3552+
if p.curTok.Type == TokenLParen {
3553+
p.nextToken() // consume (
3554+
if p.curTok.Type == TokenString {
3555+
option.Value = p.parseStringLiteralValue()
3556+
p.nextToken() // consume string
3557+
}
3558+
if p.curTok.Type == TokenRParen {
3559+
p.nextToken() // consume )
3560+
}
3561+
}
3562+
case "ELEMENTS":
3563+
// Check for XSINIL or ABSENT
3564+
nextKeyword := strings.ToUpper(p.curTok.Literal)
3565+
if nextKeyword == "XSINIL" {
3566+
option.OptionKind = "ElementsXsiNil"
3567+
p.nextToken() // consume XSINIL
3568+
} else if nextKeyword == "ABSENT" {
3569+
option.OptionKind = "ElementsAbsent"
3570+
p.nextToken() // consume ABSENT
3571+
} else {
3572+
option.OptionKind = "Elements"
3573+
}
3574+
case "XMLDATA":
3575+
option.OptionKind = "XmlData"
3576+
case "XMLSCHEMA":
3577+
option.OptionKind = "XmlSchema"
3578+
// Check for optional namespace: XMLSCHEMA ('namespace')
3579+
if p.curTok.Type == TokenLParen {
3580+
p.nextToken() // consume (
3581+
if p.curTok.Type == TokenString {
3582+
option.Value = p.parseStringLiteralValue()
3583+
p.nextToken() // consume string
3584+
}
3585+
if p.curTok.Type == TokenRParen {
3586+
p.nextToken() // consume )
3587+
}
3588+
}
3589+
case "ROOT":
3590+
option.OptionKind = "Root"
3591+
// Check for optional root name: ROOT ('name')
3592+
if p.curTok.Type == TokenLParen {
3593+
p.nextToken() // consume (
3594+
if p.curTok.Type == TokenString {
3595+
option.Value = p.parseStringLiteralValue()
3596+
p.nextToken() // consume string
3597+
}
3598+
if p.curTok.Type == TokenRParen {
3599+
p.nextToken() // consume )
3600+
}
3601+
}
3602+
case "TYPE":
3603+
option.OptionKind = "Type"
3604+
case "BINARY":
3605+
// BINARY BASE64
3606+
if strings.ToUpper(p.curTok.Literal) == "BASE64" {
3607+
option.OptionKind = "BinaryBase64"
3608+
p.nextToken() // consume BASE64
3609+
}
3610+
default:
3611+
option.OptionKind = keyword
3612+
}
3613+
3614+
return option, nil
3615+
}
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)