diff --git a/ast/create_spatial_index_statement.go b/ast/create_spatial_index_statement.go new file mode 100644 index 00000000..db960306 --- /dev/null +++ b/ast/create_spatial_index_statement.go @@ -0,0 +1,87 @@ +package ast + +// CreateSpatialIndexStatement represents a CREATE SPATIAL INDEX statement +type CreateSpatialIndexStatement struct { + Name *Identifier + Object *SchemaObjectName + SpatialColumnName *Identifier + SpatialIndexingScheme string // "None", "GeometryGrid", "GeographyGrid", "GeometryAutoGrid", "GeographyAutoGrid" + OnFileGroup *IdentifierOrValueExpression + SpatialIndexOptions []SpatialIndexOption +} + +func (s *CreateSpatialIndexStatement) node() {} +func (s *CreateSpatialIndexStatement) statement() {} + +// SpatialIndexOption is an interface for spatial index options +type SpatialIndexOption interface { + Node + spatialIndexOption() +} + +// SpatialIndexRegularOption wraps a regular IndexOption for spatial indexes +type SpatialIndexRegularOption struct { + Option IndexOption +} + +func (s *SpatialIndexRegularOption) node() {} +func (s *SpatialIndexRegularOption) spatialIndexOption() {} + +// BoundingBoxSpatialIndexOption represents a BOUNDING_BOX option +type BoundingBoxSpatialIndexOption struct { + BoundingBoxParameters []*BoundingBoxParameter +} + +func (b *BoundingBoxSpatialIndexOption) node() {} +func (b *BoundingBoxSpatialIndexOption) spatialIndexOption() {} + +// BoundingBoxParameter represents a bounding box parameter (XMIN, YMIN, XMAX, YMAX) +type BoundingBoxParameter struct { + Parameter string // "None", "XMin", "YMin", "XMax", "YMax" + Value ScalarExpression +} + +func (b *BoundingBoxParameter) node() {} + +// GridsSpatialIndexOption represents a GRIDS option +type GridsSpatialIndexOption struct { + GridParameters []*GridParameter +} + +func (g *GridsSpatialIndexOption) node() {} +func (g *GridsSpatialIndexOption) spatialIndexOption() {} + +// GridParameter represents a grid parameter +type GridParameter struct { + Parameter string // "None", "Level1", "Level2", "Level3", "Level4" + Value string // "Low", "Medium", "High" +} + +func (g *GridParameter) node() {} + +// CellsPerObjectSpatialIndexOption represents a CELLS_PER_OBJECT option +type CellsPerObjectSpatialIndexOption struct { + Value ScalarExpression +} + +func (c *CellsPerObjectSpatialIndexOption) node() {} +func (c *CellsPerObjectSpatialIndexOption) spatialIndexOption() {} + +// DataCompressionOption represents a DATA_COMPRESSION option for indexes +type DataCompressionOption struct { + CompressionLevel string // "None", "Row", "Page", "ColumnStore", "ColumnStoreArchive" + OptionKind string // "DataCompression" + PartitionRanges []*CompressionPartitionRange +} + +func (d *DataCompressionOption) node() {} +func (d *DataCompressionOption) indexOption() {} + +// IgnoreDupKeyIndexOption represents the IGNORE_DUP_KEY option +type IgnoreDupKeyIndexOption struct { + OptionState string // "On", "Off" + OptionKind string // "IgnoreDupKey" +} + +func (i *IgnoreDupKeyIndexOption) node() {} +func (i *IgnoreDupKeyIndexOption) indexOption() {} diff --git a/parser/marshal.go b/parser/marshal.go index 9a00a6ac..7ccea93e 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -346,6 +346,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return createLoginStatementToJSON(s) case *ast.CreateIndexStatement: return createIndexStatementToJSON(s) + case *ast.CreateSpatialIndexStatement: + return createSpatialIndexStatementToJSON(s) case *ast.CreateAsymmetricKeyStatement: return createAsymmetricKeyStatementToJSON(s) case *ast.CreateSymmetricKeyStatement: @@ -5185,6 +5187,108 @@ func createColumnStoreIndexStatementToJSON(s *ast.CreateColumnStoreIndexStatemen return node } +func createSpatialIndexStatementToJSON(s *ast.CreateSpatialIndexStatement) jsonNode { + node := jsonNode{ + "$type": "CreateSpatialIndexStatement", + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + if s.Object != nil { + node["Object"] = schemaObjectNameToJSON(s.Object) + } + if s.SpatialColumnName != nil { + node["SpatialColumnName"] = identifierToJSON(s.SpatialColumnName) + } + if s.SpatialIndexingScheme != "" { + node["SpatialIndexingScheme"] = s.SpatialIndexingScheme + } + if s.OnFileGroup != nil { + node["OnFileGroup"] = identifierOrValueExpressionToJSON(s.OnFileGroup) + } + if len(s.SpatialIndexOptions) > 0 { + opts := make([]jsonNode, len(s.SpatialIndexOptions)) + for i, opt := range s.SpatialIndexOptions { + opts[i] = spatialIndexOptionToJSON(opt) + } + node["SpatialIndexOptions"] = opts + } + return node +} + +func spatialIndexOptionToJSON(opt ast.SpatialIndexOption) jsonNode { + switch o := opt.(type) { + case *ast.SpatialIndexRegularOption: + node := jsonNode{ + "$type": "SpatialIndexRegularOption", + } + if o.Option != nil { + node["Option"] = indexOptionToJSON(o.Option) + } + return node + case *ast.BoundingBoxSpatialIndexOption: + node := jsonNode{ + "$type": "BoundingBoxSpatialIndexOption", + } + if len(o.BoundingBoxParameters) > 0 { + params := make([]jsonNode, len(o.BoundingBoxParameters)) + for i, p := range o.BoundingBoxParameters { + params[i] = boundingBoxParameterToJSON(p) + } + node["BoundingBoxParameters"] = params + } + return node + case *ast.GridsSpatialIndexOption: + node := jsonNode{ + "$type": "GridsSpatialIndexOption", + } + if len(o.GridParameters) > 0 { + params := make([]jsonNode, len(o.GridParameters)) + for i, p := range o.GridParameters { + params[i] = gridParameterToJSON(p) + } + node["GridParameters"] = params + } + return node + case *ast.CellsPerObjectSpatialIndexOption: + node := jsonNode{ + "$type": "CellsPerObjectSpatialIndexOption", + } + if o.Value != nil { + node["Value"] = scalarExpressionToJSON(o.Value) + } + return node + default: + return jsonNode{"$type": "UnknownSpatialIndexOption"} + } +} + +func boundingBoxParameterToJSON(p *ast.BoundingBoxParameter) jsonNode { + node := jsonNode{ + "$type": "BoundingBoxParameter", + } + if p.Parameter != "" { + node["Parameter"] = p.Parameter + } + if p.Value != nil { + node["Value"] = scalarExpressionToJSON(p.Value) + } + return node +} + +func gridParameterToJSON(p *ast.GridParameter) jsonNode { + node := jsonNode{ + "$type": "GridParameter", + } + if p.Parameter != "" { + node["Parameter"] = p.Parameter + } + if p.Value != "" { + node["Value"] = p.Value + } + return node +} + func alterFunctionStatementToJSON(s *ast.AlterFunctionStatement) jsonNode { node := jsonNode{ "$type": "AlterFunctionStatement", @@ -5378,6 +5482,26 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionKind": o.OptionKind, "Expression": scalarExpressionToJSON(o.Expression), } + case *ast.DataCompressionOption: + node := jsonNode{ + "$type": "DataCompressionOption", + "CompressionLevel": o.CompressionLevel, + "OptionKind": o.OptionKind, + } + if len(o.PartitionRanges) > 0 { + ranges := make([]jsonNode, len(o.PartitionRanges)) + for i, r := range o.PartitionRanges { + ranges[i] = compressionPartitionRangeToJSON(r) + } + node["PartitionRanges"] = ranges + } + return node + case *ast.IgnoreDupKeyIndexOption: + return jsonNode{ + "$type": "IgnoreDupKeyIndexOption", + "OptionState": o.OptionState, + "OptionKind": o.OptionKind, + } default: return jsonNode{"$type": "UnknownIndexOption"} } diff --git a/parser/parse_statements.go b/parser/parse_statements.go index a4aac69a..1aae3eee 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -1244,6 +1244,8 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { return p.parseCreateWorkloadGroupStatement() case "SEQUENCE": return p.parseCreateSequenceStatement() + case "SPATIAL": + return p.parseCreateSpatialIndexStatement() } // Lenient: skip unknown CREATE statements p.skipToEndOfStatement() @@ -4959,6 +4961,286 @@ func (p *Parser) parseCreateIndexStatement() (*ast.CreateIndexStatement, error) return stmt, nil } +func (p *Parser) parseCreateSpatialIndexStatement() (*ast.CreateSpatialIndexStatement, error) { + p.nextToken() // consume SPATIAL + if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + } + + stmt := &ast.CreateSpatialIndexStatement{ + Name: p.parseIdentifier(), + SpatialIndexingScheme: "None", + } + + // Parse ON table_name(column_name) + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + stmt.Object, _ = p.parseSchemaObjectName() + + // Parse (column_name) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + stmt.SpatialColumnName = p.parseIdentifier() + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + + // Parse USING clause for spatial indexing scheme + if strings.ToUpper(p.curTok.Literal) == "USING" { + p.nextToken() // consume USING + scheme := strings.ToUpper(p.curTok.Literal) + switch scheme { + case "GEOMETRY_GRID": + stmt.SpatialIndexingScheme = "GeometryGrid" + case "GEOGRAPHY_GRID": + stmt.SpatialIndexingScheme = "GeographyGrid" + case "GEOMETRY_AUTO_GRID": + stmt.SpatialIndexingScheme = "GeometryAutoGrid" + case "GEOGRAPHY_AUTO_GRID": + stmt.SpatialIndexingScheme = "GeographyAutoGrid" + } + p.nextToken() // consume scheme + } + + // Parse WITH clause for options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + } + + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + switch optName { + case "DATA_COMPRESSION": + compression := strings.ToUpper(p.curTok.Literal) + compressionLevel := "None" + switch compression { + case "NONE": + compressionLevel = "None" + case "ROW": + compressionLevel = "Row" + case "PAGE": + compressionLevel = "Page" + case "COLUMNSTORE": + compressionLevel = "ColumnStore" + case "COLUMNSTORE_ARCHIVE": + compressionLevel = "ColumnStoreArchive" + } + p.nextToken() // consume compression level + + opt := &ast.SpatialIndexRegularOption{ + Option: &ast.DataCompressionOption{ + CompressionLevel: compressionLevel, + OptionKind: "DataCompression", + }, + } + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, opt) + + case "BOUNDING_BOX": + bbOpt := p.parseBoundingBoxOption() + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, bbOpt) + + case "GRIDS": + gridsOpt := p.parseGridsOption() + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, gridsOpt) + + case "CELLS_PER_OBJECT": + expr, _ := p.parseScalarExpression() + cellsOpt := &ast.CellsPerObjectSpatialIndexOption{ + Value: expr, + } + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, cellsOpt) + + case "PAD_INDEX", "SORT_IN_TEMPDB", "ALLOW_ROW_LOCKS", "ALLOW_PAGE_LOCKS", "DROP_EXISTING", "ONLINE", "STATISTICS_NORECOMPUTE", "STATISTICS_INCREMENTAL": + optState := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume ON/OFF + opt := &ast.SpatialIndexRegularOption{ + Option: &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optName), + OptionState: p.capitalizeFirst(strings.ToLower(optState)), + }, + } + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, opt) + + case "MAXDOP", "FILLFACTOR": + expr, _ := p.parseScalarExpression() + opt := &ast.SpatialIndexRegularOption{ + Option: &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optName), + Expression: expr, + }, + } + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, opt) + + case "IGNORE_DUP_KEY": + optState := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume ON/OFF + opt := &ast.SpatialIndexRegularOption{ + Option: &ast.IgnoreDupKeyIndexOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(optState)), + }, + } + stmt.SpatialIndexOptions = append(stmt.SpatialIndexOptions, opt) + + default: + // Skip unknown option value + if p.curTok.Type != TokenComma && p.curTok.Type != TokenRParen { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + // Parse ON filegroup clause + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + stmt.OnFileGroup, _ = p.parseIdentifierOrValueExpression() + } + + return stmt, nil +} + +func (p *Parser) parseBoundingBoxOption() *ast.BoundingBoxSpatialIndexOption { + opt := &ast.BoundingBoxSpatialIndexOption{} + + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + } + + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + param := &ast.BoundingBoxParameter{Parameter: "None"} + + // Check if it's named parameter (XMIN, YMIN, etc.) + paramName := strings.ToUpper(p.curTok.Literal) + switch paramName { + case "XMIN": + param.Parameter = "XMin" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "YMIN": + param.Parameter = "YMin" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "XMAX": + param.Parameter = "XMax" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "YMAX": + param.Parameter = "YMax" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + } + + param.Value, _ = p.parseScalarExpression() + opt.BoundingBoxParameters = append(opt.BoundingBoxParameters, param) + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + return opt +} + +func (p *Parser) parseGridsOption() *ast.GridsSpatialIndexOption { + opt := &ast.GridsSpatialIndexOption{} + + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + } + + levelIndex := 1 + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + param := &ast.GridParameter{Parameter: "None"} + + // Check if it's named parameter (LEVEL_1, LEVEL_2, etc.) + paramName := strings.ToUpper(p.curTok.Literal) + switch paramName { + case "LEVEL_1": + param.Parameter = "Level1" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "LEVEL_2": + param.Parameter = "Level2" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "LEVEL_3": + param.Parameter = "Level3" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + case "LEVEL_4": + param.Parameter = "Level4" + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + } + + // Parse the grid value (LOW, MEDIUM, HIGH) + valueStr := strings.ToUpper(p.curTok.Literal) + switch valueStr { + case "LOW": + param.Value = "Low" + case "MEDIUM": + param.Value = "Medium" + case "HIGH": + param.Value = "High" + default: + param.Value = p.capitalizeFirst(strings.ToLower(valueStr)) + } + p.nextToken() // consume value + + opt.GridParameters = append(opt.GridParameters, param) + levelIndex++ + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + return opt +} + func (p *Parser) parseCreateAsymmetricKeyStatement() (*ast.CreateAsymmetricKeyStatement, error) { p.nextToken() // consume ASYMMETRIC if strings.ToUpper(p.curTok.Literal) == "KEY" { diff --git a/parser/testdata/Baselines110_CreateSpatialIndexStatementTests110/metadata.json b/parser/testdata/Baselines110_CreateSpatialIndexStatementTests110/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/Baselines110_CreateSpatialIndexStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_CreateSpatialIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/CreateSpatialIndexStatementTests110/metadata.json b/parser/testdata/CreateSpatialIndexStatementTests110/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/CreateSpatialIndexStatementTests110/metadata.json +++ b/parser/testdata/CreateSpatialIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file