Skip to content

Commit c4759e0

Browse files
kyleconroyclaude
andcommitted
Add full CREATE/ALTER FULLTEXT INDEX parsing support
- Add SetStopListAlterFullTextIndexAction for SET STOPLIST action - Add FullTextIndexOption interface with StopListFullTextIndexOption and ChangeTrackingFullTextIndexOption implementations - Add FullTextCatalogAndFileGroup for catalog/filegroup specification - Implement full CREATE FULLTEXT INDEX parsing with KEY INDEX, ON catalog, and WITH options (CHANGE_TRACKING, STOPLIST, NO POPULATION) - Add SET STOPLIST parsing to ALTER FULLTEXT INDEX - Fix CreateFullTextIndexStatement $type to match ScriptDOM format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f5f21dd commit c4759e0

7 files changed

Lines changed: 277 additions & 6 deletions

File tree

ast/alter_simple_statements.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,48 @@ type FullTextIndexColumn struct {
305305

306306
func (*FullTextIndexColumn) node() {}
307307

308+
// SetStopListAlterFullTextIndexAction represents a SET STOPLIST action for fulltext index
309+
type SetStopListAlterFullTextIndexAction struct {
310+
StopListOption *StopListFullTextIndexOption `json:"StopListOption,omitempty"`
311+
WithNoPopulation bool `json:"WithNoPopulation"`
312+
}
313+
314+
func (*SetStopListAlterFullTextIndexAction) node() {}
315+
func (*SetStopListAlterFullTextIndexAction) alterFullTextIndexAction() {}
316+
317+
// FullTextIndexOption is an interface for fulltext index options
318+
type FullTextIndexOption interface {
319+
fullTextIndexOption()
320+
}
321+
322+
// StopListFullTextIndexOption represents a STOPLIST option for fulltext index
323+
type StopListFullTextIndexOption struct {
324+
IsOff bool `json:"IsOff"`
325+
StopListName *Identifier `json:"StopListName,omitempty"`
326+
OptionKind string `json:"OptionKind,omitempty"` // "StopList"
327+
}
328+
329+
func (*StopListFullTextIndexOption) node() {}
330+
func (*StopListFullTextIndexOption) fullTextIndexOption() {}
331+
332+
// ChangeTrackingFullTextIndexOption represents a CHANGE_TRACKING option for fulltext index
333+
type ChangeTrackingFullTextIndexOption struct {
334+
Value string `json:"Value,omitempty"` // "Auto", "Manual", "Off", "OffNoPopulation"
335+
OptionKind string `json:"OptionKind,omitempty"` // "ChangeTracking"
336+
}
337+
338+
func (*ChangeTrackingFullTextIndexOption) node() {}
339+
func (*ChangeTrackingFullTextIndexOption) fullTextIndexOption() {}
340+
341+
// FullTextCatalogAndFileGroup represents catalog and filegroup for fulltext index
342+
type FullTextCatalogAndFileGroup struct {
343+
CatalogName *Identifier `json:"CatalogName,omitempty"`
344+
FileGroupName *Identifier `json:"FileGroupName,omitempty"`
345+
FileGroupIsFirst bool `json:"FileGroupIsFirst"`
346+
}
347+
348+
func (*FullTextCatalogAndFileGroup) node() {}
349+
308350
// AlterSymmetricKeyStatement represents an ALTER SYMMETRIC KEY statement.
309351
type AlterSymmetricKeyStatement struct {
310352
Name *Identifier `json:"Name,omitempty"`

ast/create_simple_statements.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,10 @@ func (s *CreateFulltextCatalogStatement) statement() {}
425425

426426
// CreateFulltextIndexStatement represents a CREATE FULLTEXT INDEX statement.
427427
type CreateFulltextIndexStatement struct {
428-
OnName *SchemaObjectName `json:"OnName,omitempty"`
428+
OnName *SchemaObjectName `json:"OnName,omitempty"`
429+
KeyIndexName *Identifier `json:"KeyIndexName,omitempty"`
430+
CatalogAndFileGroup *FullTextCatalogAndFileGroup `json:"CatalogAndFileGroup,omitempty"`
431+
Options []FullTextIndexOption `json:"Options,omitempty"`
429432
}
430433

431434
func (s *CreateFulltextIndexStatement) node() {}

parser/marshal.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16895,10 +16895,31 @@ func alterFullTextIndexActionToJSON(a ast.AlterFullTextIndexActionOption) jsonNo
1689516895
node["Columns"] = cols
1689616896
}
1689716897
return node
16898+
case *ast.SetStopListAlterFullTextIndexAction:
16899+
node := jsonNode{
16900+
"$type": "SetStopListAlterFullTextIndexAction",
16901+
"WithNoPopulation": action.WithNoPopulation,
16902+
}
16903+
if action.StopListOption != nil {
16904+
node["StopListOption"] = stopListFullTextIndexOptionToJSON(action.StopListOption)
16905+
}
16906+
return node
1689816907
}
1689916908
return nil
1690016909
}
1690116910

16911+
func stopListFullTextIndexOptionToJSON(opt *ast.StopListFullTextIndexOption) jsonNode {
16912+
node := jsonNode{
16913+
"$type": "StopListFullTextIndexOption",
16914+
"IsOff": opt.IsOff,
16915+
"OptionKind": opt.OptionKind,
16916+
}
16917+
if opt.StopListName != nil {
16918+
node["StopListName"] = identifierToJSON(opt.StopListName)
16919+
}
16920+
return node
16921+
}
16922+
1690216923
func fullTextIndexColumnToJSON(col *ast.FullTextIndexColumn) jsonNode {
1690316924
node := jsonNode{
1690416925
"$type": "FullTextIndexColumn",
@@ -17873,14 +17894,55 @@ func createFulltextCatalogStatementToJSON(s *ast.CreateFulltextCatalogStatement)
1787317894

1787417895
func createFulltextIndexStatementToJSON(s *ast.CreateFulltextIndexStatement) jsonNode {
1787517896
node := jsonNode{
17876-
"$type": "CreateFulltextIndexStatement",
17897+
"$type": "CreateFullTextIndexStatement",
1787717898
}
1787817899
if s.OnName != nil {
1787917900
node["OnName"] = schemaObjectNameToJSON(s.OnName)
1788017901
}
17902+
if s.KeyIndexName != nil {
17903+
node["KeyIndexName"] = identifierToJSON(s.KeyIndexName)
17904+
}
17905+
if s.CatalogAndFileGroup != nil {
17906+
node["CatalogAndFileGroup"] = fullTextCatalogAndFileGroupToJSON(s.CatalogAndFileGroup)
17907+
}
17908+
if len(s.Options) > 0 {
17909+
opts := make([]jsonNode, len(s.Options))
17910+
for i, opt := range s.Options {
17911+
opts[i] = fullTextIndexOptionToJSON(opt)
17912+
}
17913+
node["Options"] = opts
17914+
}
17915+
return node
17916+
}
17917+
17918+
func fullTextCatalogAndFileGroupToJSON(cfg *ast.FullTextCatalogAndFileGroup) jsonNode {
17919+
node := jsonNode{
17920+
"$type": "FullTextCatalogAndFileGroup",
17921+
"FileGroupIsFirst": cfg.FileGroupIsFirst,
17922+
}
17923+
if cfg.CatalogName != nil {
17924+
node["CatalogName"] = identifierToJSON(cfg.CatalogName)
17925+
}
17926+
if cfg.FileGroupName != nil {
17927+
node["FileGroupName"] = identifierToJSON(cfg.FileGroupName)
17928+
}
1788117929
return node
1788217930
}
1788317931

17932+
func fullTextIndexOptionToJSON(opt ast.FullTextIndexOption) jsonNode {
17933+
switch o := opt.(type) {
17934+
case *ast.ChangeTrackingFullTextIndexOption:
17935+
return jsonNode{
17936+
"$type": "ChangeTrackingFullTextIndexOption",
17937+
"Value": o.Value,
17938+
"OptionKind": o.OptionKind,
17939+
}
17940+
case *ast.StopListFullTextIndexOption:
17941+
return stopListFullTextIndexOptionToJSON(o)
17942+
}
17943+
return nil
17944+
}
17945+
1788417946
func createRemoteServiceBindingStatementToJSON(s *ast.CreateRemoteServiceBindingStatement) jsonNode {
1788517947
node := jsonNode{
1788617948
"$type": "CreateRemoteServiceBindingStatement",

parser/parse_ddl.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8313,8 +8313,8 @@ func (p *Parser) tryParseAlterFullTextIndexAction() ast.AlterFullTextIndexAction
83138313
return &ast.SimpleAlterFullTextIndexAction{ActionKind: "Disable"}
83148314
case "SET":
83158315
p.nextToken() // consume SET
8316-
// Parse CHANGE_TRACKING = MANUAL/AUTO/OFF
83178316
if strings.ToUpper(p.curTok.Literal) == "CHANGE_TRACKING" {
8317+
// Parse CHANGE_TRACKING = MANUAL/AUTO/OFF
83188318
p.nextToken() // consume CHANGE_TRACKING
83198319
if p.curTok.Type == TokenEquals {
83208320
p.nextToken() // consume =
@@ -8329,6 +8329,33 @@ func (p *Parser) tryParseAlterFullTextIndexAction() ast.AlterFullTextIndexAction
83298329
case "OFF":
83308330
return &ast.SimpleAlterFullTextIndexAction{ActionKind: "SetChangeTrackingOff"}
83318331
}
8332+
} else if strings.ToUpper(p.curTok.Literal) == "STOPLIST" {
8333+
// Parse SET STOPLIST OFF | SYSTEM | name [WITH NO POPULATION]
8334+
p.nextToken() // consume STOPLIST
8335+
action := &ast.SetStopListAlterFullTextIndexAction{
8336+
StopListOption: &ast.StopListFullTextIndexOption{
8337+
OptionKind: "StopList",
8338+
},
8339+
}
8340+
if strings.ToUpper(p.curTok.Literal) == "OFF" {
8341+
action.StopListOption.IsOff = true
8342+
p.nextToken()
8343+
} else {
8344+
action.StopListOption.IsOff = false
8345+
action.StopListOption.StopListName = p.parseIdentifier()
8346+
}
8347+
// Check for WITH NO POPULATION
8348+
if p.curTok.Type == TokenWith {
8349+
p.nextToken() // consume WITH
8350+
if strings.ToUpper(p.curTok.Literal) == "NO" {
8351+
p.nextToken() // consume NO
8352+
if strings.ToUpper(p.curTok.Literal) == "POPULATION" {
8353+
p.nextToken() // consume POPULATION
8354+
action.WithNoPopulation = true
8355+
}
8356+
}
8357+
}
8358+
return action
83328359
}
83338360
return nil
83348361
case "START":

parser/parse_statements.go

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12767,7 +12767,144 @@ func (p *Parser) parseCreateFulltextStatement() (ast.Statement, error) {
1276712767
stmt := &ast.CreateFulltextIndexStatement{
1276812768
OnName: onName,
1276912769
}
12770-
p.skipToEndOfStatement()
12770+
12771+
// Parse optional (column_list) - skip for now
12772+
if p.curTok.Type == TokenLParen {
12773+
p.nextToken() // consume (
12774+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
12775+
p.nextToken()
12776+
}
12777+
if p.curTok.Type == TokenRParen {
12778+
p.nextToken() // consume )
12779+
}
12780+
}
12781+
12782+
// Parse KEY INDEX name
12783+
if strings.ToUpper(p.curTok.Literal) == "KEY" {
12784+
p.nextToken() // consume KEY
12785+
if strings.ToUpper(p.curTok.Literal) == "INDEX" {
12786+
p.nextToken() // consume INDEX
12787+
}
12788+
stmt.KeyIndexName = p.parseIdentifier()
12789+
}
12790+
12791+
// Parse ON clause for catalog/filegroup
12792+
if p.curTok.Type == TokenOn {
12793+
p.nextToken() // consume ON
12794+
stmt.CatalogAndFileGroup = &ast.FullTextCatalogAndFileGroup{}
12795+
12796+
if p.curTok.Type == TokenLParen {
12797+
// (FILEGROUP fg, catalog) or (catalog, FILEGROUP fg) format
12798+
p.nextToken() // consume (
12799+
12800+
// Check first element
12801+
if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
12802+
p.nextToken() // consume FILEGROUP
12803+
stmt.CatalogAndFileGroup.FileGroupName = p.parseIdentifier()
12804+
stmt.CatalogAndFileGroup.FileGroupIsFirst = true
12805+
12806+
// Check for comma and catalog
12807+
if p.curTok.Type == TokenComma {
12808+
p.nextToken() // consume comma
12809+
stmt.CatalogAndFileGroup.CatalogName = p.parseIdentifier()
12810+
}
12811+
} else {
12812+
// It's a catalog name first
12813+
stmt.CatalogAndFileGroup.CatalogName = p.parseIdentifier()
12814+
stmt.CatalogAndFileGroup.FileGroupIsFirst = false
12815+
12816+
// Check for comma and filegroup
12817+
if p.curTok.Type == TokenComma {
12818+
p.nextToken() // consume comma
12819+
if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
12820+
p.nextToken() // consume FILEGROUP
12821+
}
12822+
stmt.CatalogAndFileGroup.FileGroupName = p.parseIdentifier()
12823+
}
12824+
}
12825+
12826+
if p.curTok.Type == TokenRParen {
12827+
p.nextToken() // consume )
12828+
}
12829+
} else {
12830+
// Just a catalog name without parentheses
12831+
stmt.CatalogAndFileGroup.CatalogName = p.parseIdentifier()
12832+
stmt.CatalogAndFileGroup.FileGroupIsFirst = false
12833+
}
12834+
}
12835+
12836+
// Parse WITH clause
12837+
if p.curTok.Type == TokenWith {
12838+
p.nextToken() // consume WITH
12839+
noPopulation := false
12840+
for {
12841+
optLit := strings.ToUpper(p.curTok.Literal)
12842+
if optLit == "CHANGE_TRACKING" {
12843+
p.nextToken() // consume CHANGE_TRACKING
12844+
var trackingValue string
12845+
if strings.ToUpper(p.curTok.Literal) == "MANUAL" {
12846+
trackingValue = "Manual"
12847+
p.nextToken()
12848+
} else if strings.ToUpper(p.curTok.Literal) == "AUTO" {
12849+
trackingValue = "Auto"
12850+
p.nextToken()
12851+
} else if strings.ToUpper(p.curTok.Literal) == "OFF" {
12852+
trackingValue = "Off"
12853+
p.nextToken()
12854+
}
12855+
// If we see NO POPULATION after CHANGE_TRACKING OFF, update the value
12856+
if trackingValue == "Off" && noPopulation {
12857+
trackingValue = "OffNoPopulation"
12858+
}
12859+
stmt.Options = append(stmt.Options, &ast.ChangeTrackingFullTextIndexOption{
12860+
Value: trackingValue,
12861+
OptionKind: "ChangeTracking",
12862+
})
12863+
} else if optLit == "STOPLIST" {
12864+
p.nextToken() // consume STOPLIST
12865+
opt := &ast.StopListFullTextIndexOption{
12866+
OptionKind: "StopList",
12867+
}
12868+
if strings.ToUpper(p.curTok.Literal) == "OFF" {
12869+
opt.IsOff = true
12870+
p.nextToken()
12871+
} else if strings.ToUpper(p.curTok.Literal) == "SYSTEM" {
12872+
opt.IsOff = false
12873+
opt.StopListName = p.parseIdentifier()
12874+
} else {
12875+
opt.IsOff = false
12876+
opt.StopListName = p.parseIdentifier()
12877+
}
12878+
stmt.Options = append(stmt.Options, opt)
12879+
} else if optLit == "NO" {
12880+
p.nextToken() // consume NO
12881+
if strings.ToUpper(p.curTok.Literal) == "POPULATION" {
12882+
p.nextToken() // consume POPULATION
12883+
noPopulation = true
12884+
// Update CHANGE_TRACKING OFF to OffNoPopulation
12885+
for i, opt := range stmt.Options {
12886+
if ctOpt, ok := opt.(*ast.ChangeTrackingFullTextIndexOption); ok && ctOpt.Value == "Off" {
12887+
ctOpt.Value = "OffNoPopulation"
12888+
stmt.Options[i] = ctOpt
12889+
}
12890+
}
12891+
}
12892+
} else {
12893+
break
12894+
}
12895+
12896+
if p.curTok.Type == TokenComma {
12897+
p.nextToken() // consume comma
12898+
} else if p.curTok.Type == TokenSemicolon || p.curTok.Type == TokenEOF {
12899+
break
12900+
}
12901+
}
12902+
}
12903+
12904+
// Skip optional semicolon
12905+
if p.curTok.Type == TokenSemicolon {
12906+
p.nextToken()
12907+
}
1277112908
return stmt, nil
1277212909
default:
1277312910
// Just create a catalog statement as default
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

parser/testdata/PhaseOne_CreateFulltextIndex/ast.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"$type": "TSqlBatch",
66
"Statements": [
77
{
8-
"$type": "CreateFulltextIndexStatement",
8+
"$type": "CreateFullTextIndexStatement",
99
"OnName": {
1010
"$type": "SchemaObjectName",
1111
"BaseIdentifier": {

0 commit comments

Comments
 (0)