Skip to content

Commit 0354255

Browse files
kyleconroyclaude
andcommitted
Add old-style table hint support and LOCAL join modifier
- Handle old-style numeric index hints: table alias (1), WITH (0) - Handle naked HOLDLOCK/NOWAIT keywords without parentheses - Handle old-style hints after alias: table alias (nolock) - Add LOCAL join modifier support (undocumented feature) - Add peekIsOldStyleIndexHint to distinguish index hints from function params - Enables FromClauseTests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9e75fd9 commit 0354255

2 files changed

Lines changed: 168 additions & 10 deletions

File tree

parser/parse_select.go

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2641,6 +2641,12 @@ func (p *Parser) parseTableReference() (ast.TableReference, error) {
26412641
break
26422642
}
26432643

2644+
// Check for LOCAL modifier (undocumented feature) and join hints
2645+
// Syntax: INNER LOCAL MERGE JOIN - LOCAL is just skipped
2646+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "LOCAL" {
2647+
p.nextToken() // skip LOCAL
2648+
}
2649+
26442650
// Check for join hints (REMOTE, LOOP, HASH, MERGE, REDUCE, REPLICATE, REDISTRIBUTE)
26452651
joinHint := ""
26462652
if p.curTok.Type == TokenIdent {
@@ -2765,6 +2771,12 @@ func (p *Parser) parseJoinTypeAndHint() (joinType, joinHint string) {
27652771
return "", ""
27662772
}
27672773

2774+
// Check for LOCAL modifier (undocumented feature) and join hints
2775+
// Syntax: INNER LOCAL MERGE JOIN - LOCAL is just skipped
2776+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "LOCAL" {
2777+
p.nextToken() // skip LOCAL
2778+
}
2779+
27682780
// Check for join hints (REMOTE, LOOP, HASH, MERGE, REDUCE, REPLICATE, REDISTRIBUTE)
27692781
if p.curTok.Type == TokenIdent {
27702782
upper := strings.ToUpper(p.curTok.Literal)
@@ -3813,27 +3825,77 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) {
38133825
}
38143826
}
38153827

3828+
// Check for naked HOLDLOCK/NOWAIT before alias: table HOLDLOCK, table2
3829+
if p.curTok.Type == TokenHoldlock {
3830+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "HoldLock"})
3831+
p.nextToken()
3832+
}
3833+
if p.curTok.Type == TokenNowait {
3834+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "Nowait"})
3835+
p.nextToken()
3836+
}
3837+
38163838
// Parse optional alias (AS alias or just alias)
38173839
if p.curTok.Type == TokenAs {
38183840
p.nextToken()
3819-
if p.curTok.Type != TokenIdent {
3841+
if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket {
3842+
ref.Alias = p.parseIdentifier()
3843+
} else {
38203844
return nil, fmt.Errorf("expected identifier after AS, got %s", p.curTok.Literal)
38213845
}
3822-
ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
3823-
p.nextToken()
3824-
} else if p.curTok.Type == TokenIdent {
3846+
} else if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket {
38253847
// Could be an alias without AS, but need to be careful not to consume keywords
3826-
upper := strings.ToUpper(p.curTok.Literal)
3827-
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && 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" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" && upper != "PIVOT" && upper != "UNPIVOT" {
3828-
ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"}
3848+
if p.curTok.Type == TokenIdent {
3849+
upper := strings.ToUpper(p.curTok.Literal)
3850+
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && 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" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" && upper != "PIVOT" && upper != "UNPIVOT" {
3851+
ref.Alias = p.parseIdentifier()
3852+
}
3853+
} else {
3854+
ref.Alias = p.parseIdentifier()
3855+
}
3856+
}
3857+
3858+
// Check for old-style hints AFTER alias: table alias (1) or table alias (nolock)
3859+
// peekIsOldStyleIndexHint is safe to use here since we're after the alias
3860+
if p.curTok.Type == TokenLParen && (p.peekIsTableHint() || p.peekIsOldStyleIndexHint()) {
3861+
p.nextToken() // consume (
3862+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
3863+
hint, err := p.parseTableHint()
3864+
if err != nil {
3865+
return nil, err
3866+
}
3867+
if hint != nil {
3868+
ref.TableHints = append(ref.TableHints, hint)
3869+
}
3870+
if p.curTok.Type == TokenComma {
3871+
p.nextToken()
3872+
} else if p.curTok.Type != TokenRParen {
3873+
if p.isTableHintToken() {
3874+
continue
3875+
}
3876+
break
3877+
}
3878+
}
3879+
if p.curTok.Type == TokenRParen {
38293880
p.nextToken()
38303881
}
38313882
}
38323883

3884+
// Check for naked HOLDLOCK/NOWAIT after alias: table alias HOLDLOCK
3885+
if p.curTok.Type == TokenHoldlock {
3886+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "HoldLock"})
3887+
p.nextToken()
3888+
}
3889+
if p.curTok.Type == TokenNowait {
3890+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "Nowait"})
3891+
p.nextToken()
3892+
}
3893+
38333894
// Check for new-style hints (with WITH keyword): alias WITH (hints)
38343895
if p.curTok.Type == TokenWith && p.peekTok.Type == TokenLParen {
38353896
p.nextToken() // consume WITH
3836-
if p.curTok.Type == TokenLParen && p.peekIsTableHint() {
3897+
// In WITH context, numbers are valid index hints: WITH (0)
3898+
if p.curTok.Type == TokenLParen && (p.peekIsTableHint() || p.peekIsOldStyleIndexHint()) {
38373899
p.nextToken() // consume (
38383900
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
38393901
hint, err := p.parseTableHint()
@@ -3915,6 +3977,16 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
39153977
}
39163978
}
39173979

3980+
// Check for naked HOLDLOCK/NOWAIT before alias: table HOLDLOCK, table2
3981+
if p.curTok.Type == TokenHoldlock {
3982+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "HoldLock"})
3983+
p.nextToken()
3984+
}
3985+
if p.curTok.Type == TokenNowait {
3986+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "Nowait"})
3987+
p.nextToken()
3988+
}
3989+
39183990
// Parse optional alias (AS alias or just alias)
39193991
if p.curTok.Type == TokenAs {
39203992
p.nextToken()
@@ -3934,6 +4006,42 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
39344006
}
39354007
}
39364008

4009+
// Check for old-style hints AFTER alias: table alias (1) or table alias (nolock)
4010+
// peekIsOldStyleIndexHint is safe to use here since we're after the alias
4011+
if p.curTok.Type == TokenLParen && (p.peekIsTableHint() || p.peekIsOldStyleIndexHint()) {
4012+
p.nextToken() // consume (
4013+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
4014+
hint, err := p.parseTableHint()
4015+
if err != nil {
4016+
return nil, err
4017+
}
4018+
if hint != nil {
4019+
ref.TableHints = append(ref.TableHints, hint)
4020+
}
4021+
if p.curTok.Type == TokenComma {
4022+
p.nextToken()
4023+
} else if p.curTok.Type != TokenRParen {
4024+
if p.isTableHintToken() {
4025+
continue
4026+
}
4027+
break
4028+
}
4029+
}
4030+
if p.curTok.Type == TokenRParen {
4031+
p.nextToken()
4032+
}
4033+
}
4034+
4035+
// Check for naked HOLDLOCK/NOWAIT after alias: table alias HOLDLOCK
4036+
if p.curTok.Type == TokenHoldlock {
4037+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "HoldLock"})
4038+
p.nextToken()
4039+
}
4040+
if p.curTok.Type == TokenNowait {
4041+
ref.TableHints = append(ref.TableHints, &ast.TableHint{HintKind: "Nowait"})
4042+
p.nextToken()
4043+
}
4044+
39374045
// Check for TABLESAMPLE after alias (supports syntax: t1 AS alias TABLESAMPLE (...))
39384046
if ref.TableSampleClause == nil && strings.ToUpper(p.curTok.Literal) == "TABLESAMPLE" {
39394047
tableSample, err := p.parseTableSampleClause()
@@ -3971,7 +4079,8 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
39714079
// Check for new-style hints (with WITH keyword): alias WITH (hints)
39724080
if p.curTok.Type == TokenWith && p.peekTok.Type == TokenLParen {
39734081
p.nextToken() // consume WITH
3974-
if p.curTok.Type == TokenLParen && p.peekIsTableHint() {
4082+
// In WITH context, numbers are valid index hints: WITH (0)
4083+
if p.curTok.Type == TokenLParen && (p.peekIsTableHint() || p.peekIsOldStyleIndexHint()) {
39754084
p.nextToken() // consume (
39764085
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
39774086
hint, err := p.parseTableHint()
@@ -4435,6 +4544,49 @@ func (p *Parser) parseSimpleExpression() (ast.ScalarExpression, error) {
44354544

44364545
// parseTableHint parses a single table hint
44374546
func (p *Parser) parseTableHint() (ast.TableHintType, error) {
4547+
// Handle old-style numeric index hint (just a number like "0" or "1")
4548+
if p.curTok.Type == TokenNumber {
4549+
hint := &ast.IndexTableHint{
4550+
HintKind: "Index",
4551+
IndexValues: []*ast.IdentifierOrValueExpression{
4552+
{
4553+
Value: p.curTok.Literal,
4554+
ValueExpression: &ast.IntegerLiteral{
4555+
LiteralType: "Integer",
4556+
Value: p.curTok.Literal,
4557+
},
4558+
},
4559+
},
4560+
}
4561+
p.nextToken()
4562+
// Check for additional comma-separated values
4563+
for p.curTok.Type == TokenComma {
4564+
p.nextToken()
4565+
if p.curTok.Type == TokenNumber {
4566+
hint.IndexValues = append(hint.IndexValues, &ast.IdentifierOrValueExpression{
4567+
Value: p.curTok.Literal,
4568+
ValueExpression: &ast.IntegerLiteral{
4569+
LiteralType: "Integer",
4570+
Value: p.curTok.Literal,
4571+
},
4572+
})
4573+
p.nextToken()
4574+
} else if p.curTok.Type == TokenIdent {
4575+
hint.IndexValues = append(hint.IndexValues, &ast.IdentifierOrValueExpression{
4576+
Value: p.curTok.Literal,
4577+
Identifier: &ast.Identifier{
4578+
Value: p.curTok.Literal,
4579+
QuoteType: "NotQuoted",
4580+
},
4581+
})
4582+
p.nextToken()
4583+
} else {
4584+
break
4585+
}
4586+
}
4587+
return hint, nil
4588+
}
4589+
44384590
hintName := strings.ToUpper(p.curTok.Literal)
44394591
p.nextToken() // consume hint name
44404592

@@ -4686,6 +4838,12 @@ func (p *Parser) peekIsTableHint() bool {
46864838
return false
46874839
}
46884840

4841+
// peekIsOldStyleIndexHint checks if the peek token is a number (for old-style index hint like (0))
4842+
// This is only valid after an alias or table name, not for function calls
4843+
func (p *Parser) peekIsOldStyleIndexHint() bool {
4844+
return p.peekTok.Type == TokenNumber
4845+
}
4846+
46894847
func (p *Parser) parseSchemaObjectName() (*ast.SchemaObjectName, error) {
46904848
var identifiers []*ast.Identifier
46914849

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)