Skip to content

Commit 67abba5

Browse files
committed
Add NONCLUSTERED HASH and BUCKET_COUNT support for constraints
- Parse HASH suffix after NONCLUSTERED in column-level and table-level constraints - Parse WITH (BUCKET_COUNT = N) index options for PRIMARY KEY and UNIQUE constraints - Add MEMORY_OPTIMIZED table option parsing for CREATE TABLE - Fix Clustered field output in JSON - only emit for non-hash index types - Enable Baselines120_UniqueConstraintTests120 and UniqueConstraintTests120 tests
1 parent 0b52803 commit 67abba5

5 files changed

Lines changed: 193 additions & 14 deletions

File tree

parser/marshal.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3140,6 +3140,20 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error)
31403140
DataCompressionOption: opt,
31413141
OptionKind: "DataCompression",
31423142
})
3143+
} else if optionName == "MEMORY_OPTIMIZED" {
3144+
if p.curTok.Type == TokenEquals {
3145+
p.nextToken() // consume =
3146+
}
3147+
stateUpper := strings.ToUpper(p.curTok.Literal)
3148+
state := "On"
3149+
if stateUpper == "OFF" {
3150+
state = "Off"
3151+
}
3152+
p.nextToken() // consume ON/OFF
3153+
stmt.Options = append(stmt.Options, &ast.MemoryOptimizedTableOption{
3154+
OptionKind: "MemoryOptimized",
3155+
OptionState: state,
3156+
})
31433157
} else {
31443158
// Skip unknown option value
31453159
if p.curTok.Type == TokenEquals {
@@ -3352,8 +3366,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) {
33523366
p.nextToken()
33533367
} else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" {
33543368
constraint.Clustered = false
3355-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
33563369
p.nextToken()
3370+
// Check for HASH suffix
3371+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
3372+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
3373+
p.nextToken()
3374+
} else {
3375+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
3376+
}
33573377
}
33583378
// Parse WITH (index_options)
33593379
if strings.ToUpper(p.curTok.Literal) == "WITH" {
@@ -3384,8 +3404,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) {
33843404
p.nextToken()
33853405
} else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" {
33863406
constraint.Clustered = false
3387-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
33883407
p.nextToken()
3408+
// Check for HASH suffix
3409+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
3410+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
3411+
p.nextToken()
3412+
} else {
3413+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
3414+
}
33893415
}
33903416
// Parse WITH (index_options)
33913417
if strings.ToUpper(p.curTok.Literal) == "WITH" {
@@ -4798,8 +4824,10 @@ func uniqueConstraintToJSON(c *ast.UniqueConstraintDefinition) jsonNode {
47984824
"$type": "UniqueConstraintDefinition",
47994825
"IsPrimaryKey": c.IsPrimaryKey,
48004826
}
4801-
// Output Clustered if it's true, or if IndexType is set (meaning NONCLUSTERED was explicitly specified)
4802-
if c.Clustered || c.IndexType != nil {
4827+
// Output Clustered if it's true, or if IndexType is NonClustered (not Hash variants)
4828+
if c.Clustered {
4829+
node["Clustered"] = c.Clustered
4830+
} else if c.IndexType != nil && (c.IndexType.IndexTypeKind == "NonClustered" || c.IndexType.IndexTypeKind == "Clustered") {
48034831
node["Clustered"] = c.Clustered
48044832
}
48054833
// Output IsEnforced if it's explicitly set
@@ -7554,6 +7582,7 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) {
75547582

75557583
func (p *Parser) getIndexOptionKind(optionName string) string {
75567584
optionMap := map[string]string{
7585+
"BUCKET_COUNT": "BucketCount",
75577586
"PAD_INDEX": "PadIndex",
75587587
"FILLFACTOR": "FillFactor",
75597588
"SORT_IN_TEMPDB": "SortInTempDB",

parser/parse_ddl.go

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2915,7 +2915,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
29152915
ConstraintIdentifier: constraintName,
29162916
IsPrimaryKey: true,
29172917
}
2918-
// Parse optional CLUSTERED/NONCLUSTERED
2918+
// Parse optional CLUSTERED/NONCLUSTERED/HASH
29192919
for {
29202920
upperOpt := strings.ToUpper(p.curTok.Literal)
29212921
if upperOpt == "CLUSTERED" {
@@ -2924,7 +2924,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
29242924
p.nextToken()
29252925
} else if upperOpt == "NONCLUSTERED" {
29262926
constraint.Clustered = false
2927-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
2927+
p.nextToken()
2928+
// Check for HASH suffix
2929+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
2930+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
2931+
p.nextToken()
2932+
} else {
2933+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
2934+
}
2935+
} else if upperOpt == "HASH" {
2936+
// HASH without NONCLUSTERED
2937+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
29282938
p.nextToken()
29292939
} else {
29302940
break
@@ -2967,6 +2977,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
29672977
p.nextToken()
29682978
}
29692979
}
2980+
// Parse WITH (index_options)
2981+
if p.curTok.Type == TokenWith {
2982+
p.nextToken() // consume WITH
2983+
if p.curTok.Type == TokenLParen {
2984+
p.nextToken() // consume (
2985+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
2986+
optionName := strings.ToUpper(p.curTok.Literal)
2987+
p.nextToken()
2988+
if p.curTok.Type == TokenEquals {
2989+
p.nextToken() // consume =
2990+
}
2991+
expr, _ := p.parseScalarExpression()
2992+
option := &ast.IndexExpressionOption{
2993+
OptionKind: convertIndexOptionKind(optionName),
2994+
Expression: expr,
2995+
}
2996+
constraint.IndexOptions = append(constraint.IndexOptions, option)
2997+
if p.curTok.Type == TokenComma {
2998+
p.nextToken()
2999+
} else {
3000+
break
3001+
}
3002+
}
3003+
if p.curTok.Type == TokenRParen {
3004+
p.nextToken()
3005+
}
3006+
}
3007+
}
29703008
// Parse NOT ENFORCED
29713009
if strings.ToUpper(p.curTok.Literal) == "NOT" {
29723010
p.nextToken()
@@ -2987,7 +3025,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
29873025
ConstraintIdentifier: constraintName,
29883026
IsPrimaryKey: false,
29893027
}
2990-
// Parse optional CLUSTERED/NONCLUSTERED
3028+
// Parse optional CLUSTERED/NONCLUSTERED/HASH
29913029
for {
29923030
upperOpt := strings.ToUpper(p.curTok.Literal)
29933031
if upperOpt == "CLUSTERED" {
@@ -2996,7 +3034,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
29963034
p.nextToken()
29973035
} else if upperOpt == "NONCLUSTERED" {
29983036
constraint.Clustered = false
2999-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
3037+
p.nextToken()
3038+
// Check for HASH suffix
3039+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
3040+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
3041+
p.nextToken()
3042+
} else {
3043+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
3044+
}
3045+
} else if upperOpt == "HASH" {
3046+
// HASH without NONCLUSTERED
3047+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
30003048
p.nextToken()
30013049
} else {
30023050
break
@@ -3039,6 +3087,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (*
30393087
p.nextToken()
30403088
}
30413089
}
3090+
// Parse WITH (index_options)
3091+
if p.curTok.Type == TokenWith {
3092+
p.nextToken() // consume WITH
3093+
if p.curTok.Type == TokenLParen {
3094+
p.nextToken() // consume (
3095+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
3096+
optionName := strings.ToUpper(p.curTok.Literal)
3097+
p.nextToken()
3098+
if p.curTok.Type == TokenEquals {
3099+
p.nextToken() // consume =
3100+
}
3101+
expr, _ := p.parseScalarExpression()
3102+
option := &ast.IndexExpressionOption{
3103+
OptionKind: convertIndexOptionKind(optionName),
3104+
Expression: expr,
3105+
}
3106+
constraint.IndexOptions = append(constraint.IndexOptions, option)
3107+
if p.curTok.Type == TokenComma {
3108+
p.nextToken()
3109+
} else {
3110+
break
3111+
}
3112+
}
3113+
if p.curTok.Type == TokenRParen {
3114+
p.nextToken()
3115+
}
3116+
}
3117+
}
30423118
// Parse NOT ENFORCED
30433119
if strings.ToUpper(p.curTok.Literal) == "NOT" {
30443120
p.nextToken()

parser/parse_statements.go

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,23 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) {
238238
constraint := &ast.UniqueConstraintDefinition{
239239
IsPrimaryKey: true,
240240
}
241-
// Parse optional CLUSTERED/NONCLUSTERED
241+
// Parse optional CLUSTERED/NONCLUSTERED/HASH
242242
if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" {
243243
constraint.Clustered = true
244244
constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"}
245245
p.nextToken()
246246
} else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" {
247247
constraint.Clustered = false
248-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
248+
p.nextToken()
249+
// Check for HASH suffix
250+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
251+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
252+
p.nextToken()
253+
} else {
254+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
255+
}
256+
} else if strings.ToUpper(p.curTok.Literal) == "HASH" {
257+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
249258
p.nextToken()
250259
}
251260
// Parse the column list
@@ -285,20 +294,57 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) {
285294
p.nextToken()
286295
}
287296
}
297+
// Parse WITH (index_options)
298+
if p.curTok.Type == TokenWith {
299+
p.nextToken() // consume WITH
300+
if p.curTok.Type == TokenLParen {
301+
p.nextToken() // consume (
302+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
303+
optionName := strings.ToUpper(p.curTok.Literal)
304+
p.nextToken()
305+
if p.curTok.Type == TokenEquals {
306+
p.nextToken() // consume =
307+
}
308+
expr, _ := p.parseScalarExpression()
309+
option := &ast.IndexExpressionOption{
310+
OptionKind: p.getIndexOptionKind(optionName),
311+
Expression: expr,
312+
}
313+
constraint.IndexOptions = append(constraint.IndexOptions, option)
314+
if p.curTok.Type == TokenComma {
315+
p.nextToken()
316+
} else {
317+
break
318+
}
319+
}
320+
if p.curTok.Type == TokenRParen {
321+
p.nextToken()
322+
}
323+
}
324+
}
288325
return constraint, nil
289326
} else if upperLit == "UNIQUE" {
290327
p.nextToken() // consume UNIQUE
291328
constraint := &ast.UniqueConstraintDefinition{
292329
IsPrimaryKey: false,
293330
}
294-
// Parse optional CLUSTERED/NONCLUSTERED
331+
// Parse optional CLUSTERED/NONCLUSTERED/HASH
295332
if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" {
296333
constraint.Clustered = true
297334
constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"}
298335
p.nextToken()
299336
} else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" {
300337
constraint.Clustered = false
301-
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
338+
p.nextToken()
339+
// Check for HASH suffix
340+
if strings.ToUpper(p.curTok.Literal) == "HASH" {
341+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
342+
p.nextToken()
343+
} else {
344+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"}
345+
}
346+
} else if strings.ToUpper(p.curTok.Literal) == "HASH" {
347+
constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"}
302348
p.nextToken()
303349
}
304350
// Parse the column list
@@ -338,6 +384,34 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) {
338384
p.nextToken()
339385
}
340386
}
387+
// Parse WITH (index_options)
388+
if p.curTok.Type == TokenWith {
389+
p.nextToken() // consume WITH
390+
if p.curTok.Type == TokenLParen {
391+
p.nextToken() // consume (
392+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
393+
optionName := strings.ToUpper(p.curTok.Literal)
394+
p.nextToken()
395+
if p.curTok.Type == TokenEquals {
396+
p.nextToken() // consume =
397+
}
398+
expr, _ := p.parseScalarExpression()
399+
option := &ast.IndexExpressionOption{
400+
OptionKind: p.getIndexOptionKind(optionName),
401+
Expression: expr,
402+
}
403+
constraint.IndexOptions = append(constraint.IndexOptions, option)
404+
if p.curTok.Type == TokenComma {
405+
p.nextToken()
406+
} else {
407+
break
408+
}
409+
}
410+
if p.curTok.Type == TokenRParen {
411+
p.nextToken()
412+
}
413+
}
414+
}
341415
return constraint, nil
342416
} else if upperLit == "FOREIGN" {
343417
p.nextToken() // consume FOREIGN
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)