From 67abba5b355a54505b7ee8413d4404ed6ffe2fb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:04:59 +0000 Subject: [PATCH 01/29] 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 --- parser/marshal.go | 37 +++++++- parser/parse_ddl.go | 84 ++++++++++++++++++- parser/parse_statements.go | 82 +++++++++++++++++- .../metadata.json | 2 +- .../UniqueConstraintTests120/metadata.json | 2 +- 5 files changed, 193 insertions(+), 14 deletions(-) diff --git a/parser/marshal.go b/parser/marshal.go index b41c6acd..e619a95a 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -3140,6 +3140,20 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) DataCompressionOption: opt, OptionKind: "DataCompression", }) + } else if optionName == "MEMORY_OPTIMIZED" { + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + stateUpper := strings.ToUpper(p.curTok.Literal) + state := "On" + if stateUpper == "OFF" { + state = "Off" + } + p.nextToken() // consume ON/OFF + stmt.Options = append(stmt.Options, &ast.MemoryOptimizedTableOption{ + OptionKind: "MemoryOptimized", + OptionState: state, + }) } else { // Skip unknown option value if p.curTok.Type == TokenEquals { @@ -3352,8 +3366,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } } // Parse WITH (index_options) if strings.ToUpper(p.curTok.Literal) == "WITH" { @@ -3384,8 +3404,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } } // Parse WITH (index_options) if strings.ToUpper(p.curTok.Literal) == "WITH" { @@ -4798,8 +4824,10 @@ func uniqueConstraintToJSON(c *ast.UniqueConstraintDefinition) jsonNode { "$type": "UniqueConstraintDefinition", "IsPrimaryKey": c.IsPrimaryKey, } - // Output Clustered if it's true, or if IndexType is set (meaning NONCLUSTERED was explicitly specified) - if c.Clustered || c.IndexType != nil { + // Output Clustered if it's true, or if IndexType is NonClustered (not Hash variants) + if c.Clustered { + node["Clustered"] = c.Clustered + } else if c.IndexType != nil && (c.IndexType.IndexTypeKind == "NonClustered" || c.IndexType.IndexTypeKind == "Clustered") { node["Clustered"] = c.Clustered } // Output IsEnforced if it's explicitly set @@ -7554,6 +7582,7 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { func (p *Parser) getIndexOptionKind(optionName string) string { optionMap := map[string]string{ + "BUCKET_COUNT": "BucketCount", "PAD_INDEX": "PadIndex", "FILLFACTOR": "FillFactor", "SORT_IN_TEMPDB": "SortInTempDB", diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index 5133ceea..88dc423f 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -2915,7 +2915,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* ConstraintIdentifier: constraintName, IsPrimaryKey: true, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH for { upperOpt := strings.ToUpper(p.curTok.Literal) if upperOpt == "CLUSTERED" { @@ -2924,7 +2924,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } else if upperOpt == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if upperOpt == "HASH" { + // HASH without NONCLUSTERED + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } else { break @@ -2967,6 +2977,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } } + // Parse WITH (index_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 { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: convertIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } // Parse NOT ENFORCED if strings.ToUpper(p.curTok.Literal) == "NOT" { p.nextToken() @@ -2987,7 +3025,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* ConstraintIdentifier: constraintName, IsPrimaryKey: false, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH for { upperOpt := strings.ToUpper(p.curTok.Literal) if upperOpt == "CLUSTERED" { @@ -2996,7 +3034,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } else if upperOpt == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if upperOpt == "HASH" { + // HASH without NONCLUSTERED + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } else { break @@ -3039,6 +3087,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } } + // Parse WITH (index_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 { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: convertIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } // Parse NOT ENFORCED if strings.ToUpper(p.curTok.Literal) == "NOT" { p.nextToken() diff --git a/parser/parse_statements.go b/parser/parse_statements.go index ccee32e0..8e2c2582 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -238,14 +238,23 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { constraint := &ast.UniqueConstraintDefinition{ IsPrimaryKey: true, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { constraint.Clustered = true constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } // Parse the column list @@ -285,20 +294,57 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { p.nextToken() } } + // Parse WITH (index_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 { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } return constraint, nil } else if upperLit == "UNIQUE" { p.nextToken() // consume UNIQUE constraint := &ast.UniqueConstraintDefinition{ IsPrimaryKey: false, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { constraint.Clustered = true constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } // Parse the column list @@ -338,6 +384,34 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { p.nextToken() } } + // Parse WITH (index_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 { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } return constraint, nil } else if upperLit == "FOREIGN" { p.nextToken() // consume FOREIGN diff --git a/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json b/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json +++ b/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/UniqueConstraintTests120/metadata.json b/parser/testdata/UniqueConstraintTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/UniqueConstraintTests120/metadata.json +++ b/parser/testdata/UniqueConstraintTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 17dba0982766aa0177135a88a09c78e4bbb42e64 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:07:08 +0000 Subject: [PATCH 02/29] Fix encoding for CreateMessageTypeStatementTests query.sql Convert query.sql from UTF-16LE to UTF-8 to enable proper parsing. --- .../metadata.json | 2 +- .../CreateMessageTypeStatementTests/query.sql | Bin 550 -> 271 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/testdata/CreateMessageTypeStatementTests/metadata.json b/parser/testdata/CreateMessageTypeStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateMessageTypeStatementTests/metadata.json +++ b/parser/testdata/CreateMessageTypeStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateMessageTypeStatementTests/query.sql b/parser/testdata/CreateMessageTypeStatementTests/query.sql index 36f818abe8f4d4cb172f3dd87bead9c22d5f810c..38875b952f0589aefc12aba27b904b29971b19cd 100644 GIT binary patch literal 271 zcmZ{e(F%ev7=`b9ivI;__yApu`Io|$;YJn_QZ`Uxo46U}(?`;cke3I}`Oe33qzXZx zlyr-rPJ4tiA@u&#HuRld7Os2vRpku9SF5epp2+@Yn(&JkI8dI(RHfMt)=(hlY<5VVF{^vwChixznZ%@44obIqtqrP^2mW$r2P4SjIWB JQQlW|^a0ajQB(i` literal 550 zcmbV}-AV#c6otQQp?8=Uu;>B07<9^kF~u3uh=_g$l<idBWPdR@2;#BCBm2Q!@+9@`Sm6Z&>QeLKR;pbUmi^FmdnJ*3HzH?zm&FZymku V>UbpX1(y?1A!p}*<$I2G?FTHUQ+NOX From 94eaa07beb1aaaba17133ab39ff3a8eab9322adf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:10:35 +0000 Subject: [PATCH 03/29] Add COMPRESS_ALL_ROW_GROUPS and COMPRESSION_DELAY index options - Add COMPRESS_ALL_ROW_GROUPS option mapping for ALTER INDEX REORGANIZE - Implement CompressionDelayIndexOption parsing with TimeUnit support - Handle MINUTE/MINUTES time unit suffix for COMPRESSION_DELAY - Enable Baselines130_AlterIndexStatementTests130 and AlterIndexStatementTests130 tests --- parser/marshal.go | 36 +++++++++++++++++-- parser/parse_ddl.go | 26 +++++++------- .../AlterIndexStatementTests130/metadata.json | 2 +- .../metadata.json | 2 +- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/parser/marshal.go b/parser/marshal.go index e619a95a..a326ba87 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -7469,13 +7469,31 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { if p.curTok.Type == TokenEquals { p.nextToken() - valueStr := strings.ToUpper(p.curTok.Literal) + valueStr := p.curTok.Literal + valueUpper := strings.ToUpper(valueStr) p.nextToken() - if valueStr == "ON" || valueStr == "OFF" { + if optionName == "COMPRESSION_DELAY" { + // Parse COMPRESSION_DELAY = value [MINUTE|MINUTES] + timeUnit := "Unitless" + nextUpper := strings.ToUpper(p.curTok.Literal) + if nextUpper == "MINUTE" { + timeUnit = "Minute" + p.nextToken() + } else if nextUpper == "MINUTES" { + timeUnit = "Minutes" + p.nextToken() + } + opt := &ast.CompressionDelayIndexOption{ + OptionKind: "CompressionDelay", + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, + TimeUnit: timeUnit, + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else if valueUpper == "ON" || valueUpper == "OFF" { opt := &ast.IndexStateOption{ OptionKind: p.getIndexOptionKind(optionName), - OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), } stmt.IndexOptions = append(stmt.IndexOptions, opt) } else { @@ -7598,6 +7616,8 @@ func (p *Parser) getIndexOptionKind(optionName string) string { "MAX_DURATION": "MaxDuration", "WAIT_AT_LOW_PRIORITY": "WaitAtLowPriority", "OPTIMIZE_FOR_SEQUENTIAL_KEY": "OptimizeForSequentialKey", + "COMPRESS_ALL_ROW_GROUPS": "CompressAllRowGroups", + "COMPRESSION_DELAY": "CompressionDelay", } if kind, ok := optionMap[optionName]; ok { return kind @@ -9311,6 +9331,16 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionState": o.OptionState, "OptionKind": o.OptionKind, } + case *ast.CompressionDelayIndexOption: + node := jsonNode{ + "$type": "CompressionDelayIndexOption", + "OptionKind": o.OptionKind, + "TimeUnit": o.TimeUnit, + } + if o.Expression != nil { + node["Expression"] = scalarExpressionToJSON(o.Expression) + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index 88dc423f..181df612 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -2772,18 +2772,20 @@ func (p *Parser) parseAlterTableAlterIndexStatement(tableName *ast.SchemaObjectN func convertIndexOptionKind(name string) string { optionMap := map[string]string{ - "BUCKET_COUNT": "BucketCount", - "PAD_INDEX": "PadIndex", - "FILLFACTOR": "FillFactor", - "SORT_IN_TEMPDB": "SortInTempDB", - "IGNORE_DUP_KEY": "IgnoreDupKey", - "STATISTICS_NORECOMPUTE": "StatisticsNoRecompute", - "DROP_EXISTING": "DropExisting", - "ONLINE": "Online", - "ALLOW_ROW_LOCKS": "AllowRowLocks", - "ALLOW_PAGE_LOCKS": "AllowPageLocks", - "MAXDOP": "MaxDop", - "DATA_COMPRESSION": "DataCompression", + "BUCKET_COUNT": "BucketCount", + "PAD_INDEX": "PadIndex", + "FILLFACTOR": "FillFactor", + "SORT_IN_TEMPDB": "SortInTempDB", + "IGNORE_DUP_KEY": "IgnoreDupKey", + "STATISTICS_NORECOMPUTE": "StatisticsNoRecompute", + "DROP_EXISTING": "DropExisting", + "ONLINE": "Online", + "ALLOW_ROW_LOCKS": "AllowRowLocks", + "ALLOW_PAGE_LOCKS": "AllowPageLocks", + "MAXDOP": "MaxDop", + "DATA_COMPRESSION": "DataCompression", + "COMPRESS_ALL_ROW_GROUPS": "CompressAllRowGroups", + "COMPRESSION_DELAY": "CompressionDelay", } if mapped, ok := optionMap[name]; ok { return mapped diff --git a/parser/testdata/AlterIndexStatementTests130/metadata.json b/parser/testdata/AlterIndexStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterIndexStatementTests130/metadata.json +++ b/parser/testdata/AlterIndexStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json b/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json +++ b/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From d06e08075003520ce7376d09bb76b1d1655b2095 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:20:56 +0000 Subject: [PATCH 04/29] Add CREATE MATERIALIZED VIEW support with distribution and options - Add parseCreateMaterializedViewStatement for MATERIALIZED VIEW parsing - Add ViewOption interface with ViewStatementOption, ViewDistributionOption, and ViewForAppendOption types - Parse DISTRIBUTION = HASH(columns) and FOR_APPEND options - Use $ref for duplicate identifiers in DistributionColumns - Enable MaterializedViewTests130/140/150/160 and Baselines* variants --- ast/create_view_statement.go | 32 +++++- parser/marshal.go | 51 +++++++++ parser/parse_statements.go | 102 +++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../MaterializedViewTests140/metadata.json | 2 +- .../MaterializedViewTests150/metadata.json | 2 +- .../MaterializedViewTests160/metadata.json | 2 +- 9 files changed, 188 insertions(+), 9 deletions(-) diff --git a/ast/create_view_statement.go b/ast/create_view_statement.go index 951a1ef4..44e69ae7 100644 --- a/ast/create_view_statement.go +++ b/ast/create_view_statement.go @@ -13,7 +13,35 @@ type CreateViewStatement struct { func (c *CreateViewStatement) node() {} func (c *CreateViewStatement) statement() {} -// ViewOption represents a view option like SCHEMABINDING. -type ViewOption struct { +// ViewOption is an interface for different view option types. +type ViewOption interface { + viewOption() +} + +// ViewStatementOption represents a simple view option like SCHEMABINDING. +type ViewStatementOption struct { + OptionKind string `json:"OptionKind,omitempty"` +} + +func (v *ViewStatementOption) viewOption() {} + +// ViewDistributionOption represents a DISTRIBUTION option for materialized views. +type ViewDistributionOption struct { + OptionKind string `json:"OptionKind,omitempty"` + Value *ViewHashDistributionPolicy `json:"Value,omitempty"` +} + +func (v *ViewDistributionOption) viewOption() {} + +// ViewHashDistributionPolicy represents the hash distribution policy for materialized views. +type ViewHashDistributionPolicy struct { + DistributionColumn *Identifier `json:"DistributionColumn,omitempty"` + DistributionColumns []*Identifier `json:"DistributionColumns,omitempty"` +} + +// ViewForAppendOption represents the FOR_APPEND option for materialized views. +type ViewForAppendOption struct { OptionKind string `json:"OptionKind,omitempty"` } + +func (v *ViewForAppendOption) viewOption() {} diff --git a/parser/marshal.go b/parser/marshal.go index a326ba87..80cf3480 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -2817,6 +2817,13 @@ func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { } node["Columns"] = cols } + if len(s.ViewOptions) > 0 { + opts := make([]jsonNode, len(s.ViewOptions)) + for i, opt := range s.ViewOptions { + opts[i] = viewOptionToJSON(opt) + } + node["ViewOptions"] = opts + } if s.SelectStatement != nil { node["SelectStatement"] = selectStatementToJSON(s.SelectStatement) } @@ -2825,6 +2832,50 @@ func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { return node } +func viewOptionToJSON(opt ast.ViewOption) jsonNode { + switch o := opt.(type) { + case *ast.ViewStatementOption: + return jsonNode{ + "$type": "ViewOption", + "OptionKind": o.OptionKind, + } + case *ast.ViewDistributionOption: + node := jsonNode{ + "$type": "ViewDistributionOption", + "OptionKind": o.OptionKind, + } + if o.Value != nil { + valueNode := jsonNode{ + "$type": "ViewHashDistributionPolicy", + } + if o.Value.DistributionColumn != nil { + valueNode["DistributionColumn"] = identifierToJSON(o.Value.DistributionColumn) + } + if len(o.Value.DistributionColumns) > 0 { + cols := make([]jsonNode, len(o.Value.DistributionColumns)) + for i, c := range o.Value.DistributionColumns { + // First column is same as DistributionColumn, use $ref + if i == 0 && o.Value.DistributionColumn != nil { + cols[i] = jsonNode{"$ref": "Identifier"} + } else { + cols[i] = identifierToJSON(c) + } + } + valueNode["DistributionColumns"] = cols + } + node["Value"] = valueNode + } + return node + case *ast.ViewForAppendOption: + return jsonNode{ + "$type": "ViewForAppendOption", + "OptionKind": o.OptionKind, + } + default: + return jsonNode{"$type": "UnknownViewOption"} + } +} + func createSchemaStatementToJSON(s *ast.CreateSchemaStatement) jsonNode { node := jsonNode{ "$type": "CreateSchemaStatement", diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 8e2c2582..dbddf20a 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -1789,6 +1789,8 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { return p.parseCreateSequenceStatement() case "SPATIAL": return p.parseCreateSpatialIndexStatement() + case "MATERIALIZED": + return p.parseCreateMaterializedViewStatement() case "SERVER": // Check if it's SERVER ROLE or SERVER AUDIT p.nextToken() // consume SERVER @@ -2899,7 +2901,7 @@ func (p *Parser) parseCreateViewStatement() (*ast.CreateViewStatement, error) { p.nextToken() // Parse view options for p.curTok.Type == TokenIdent { - opt := ast.ViewOption{OptionKind: p.curTok.Literal} + opt := &ast.ViewStatementOption{OptionKind: p.curTok.Literal} stmt.ViewOptions = append(stmt.ViewOptions, opt) p.nextToken() if p.curTok.Type == TokenComma { @@ -2927,6 +2929,104 @@ func (p *Parser) parseCreateViewStatement() (*ast.CreateViewStatement, error) { return stmt, nil } +func (p *Parser) parseCreateMaterializedViewStatement() (*ast.CreateViewStatement, error) { + // Consume MATERIALIZED + p.nextToken() + + // Expect VIEW + if p.curTok.Type != TokenView { + return nil, fmt.Errorf("expected VIEW after MATERIALIZED, got %s", p.curTok.Literal) + } + p.nextToken() + + stmt := &ast.CreateViewStatement{ + IsMaterialized: true, + } + + // Parse view name + son, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + stmt.SchemaObjectName = son + + // Parse WITH options for materialized view + if p.curTok.Type == TokenWith || strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if optionName == "DISTRIBUTION" { + // Parse DISTRIBUTION = HASH(col1, col2, ...) + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "HASH" { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + distOpt := &ast.ViewDistributionOption{ + OptionKind: "Distribution", + Value: &ast.ViewHashDistributionPolicy{}, + } + // Parse column list + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col := p.parseIdentifier() + if distOpt.Value.DistributionColumn == nil { + distOpt.Value.DistributionColumn = col + } + distOpt.Value.DistributionColumns = append(distOpt.Value.DistributionColumns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + stmt.ViewOptions = append(stmt.ViewOptions, distOpt) + } + } + } else if optionName == "FOR_APPEND" { + stmt.ViewOptions = append(stmt.ViewOptions, &ast.ViewForAppendOption{ + OptionKind: "ForAppend", + }) + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } else if p.curTok.Type != TokenRParen { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } + + // Expect AS + if p.curTok.Type != TokenAs { + p.skipToEndOfStatement() + return stmt, nil + } + p.nextToken() + + // Parse SELECT statement + selStmt, err := p.parseSelectStatement() + if err != nil { + p.skipToEndOfStatement() + return stmt, nil + } + stmt.SelectStatement = selStmt + + return stmt, nil +} + func (p *Parser) parseCreateSchemaStatement() (*ast.CreateSchemaStatement, error) { // Consume SCHEMA p.nextToken() diff --git a/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json b/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json +++ b/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json b/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json +++ b/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json b/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json +++ b/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests140/metadata.json b/parser/testdata/MaterializedViewTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests140/metadata.json +++ b/parser/testdata/MaterializedViewTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests150/metadata.json b/parser/testdata/MaterializedViewTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests150/metadata.json +++ b/parser/testdata/MaterializedViewTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests160/metadata.json b/parser/testdata/MaterializedViewTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests160/metadata.json +++ b/parser/testdata/MaterializedViewTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From df16bfcfc6eba70c6ad62a61d16beb0d5fdfaac3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:24:22 +0000 Subject: [PATCH 05/29] Add pseudo column parsing for $ACTION, $CUID, $ROWGUID Handle pseudo columns in INSERT column list with proper ColumnType: - $ACTION -> PseudoColumnAction - $CUID -> PseudoColumnCuid - $ROWGUID -> PseudoColumnRowGuid Enable Baselines100_InsertStatementTests100 and InsertStatementTests100 tests. --- parser/parse_dml.go | 22 +++++++++++++++---- .../metadata.json | 2 +- .../InsertStatementTests100/metadata.json | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/parser/parse_dml.go b/parser/parse_dml.go index d1a04796..507c47d0 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -656,11 +656,25 @@ func (p *Parser) parseColumnList() ([]*ast.ColumnReferenceExpression, error) { var cols []*ast.ColumnReferenceExpression for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - col, err := p.parseMultiPartIdentifierAsColumn() - if err != nil { - return nil, err + // Check for pseudo columns + lit := p.curTok.Literal + upperLit := strings.ToUpper(lit) + if upperLit == "$ACTION" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnAction"}) + p.nextToken() + } else if upperLit == "$CUID" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnCuid"}) + p.nextToken() + } else if upperLit == "$ROWGUID" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnRowGuid"}) + p.nextToken() + } else { + col, err := p.parseMultiPartIdentifierAsColumn() + if err != nil { + return nil, err + } + cols = append(cols, col) } - cols = append(cols, col) if p.curTok.Type != TokenComma { break diff --git a/parser/testdata/Baselines100_InsertStatementTests100/metadata.json b/parser/testdata/Baselines100_InsertStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_InsertStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_InsertStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/InsertStatementTests100/metadata.json b/parser/testdata/InsertStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/InsertStatementTests100/metadata.json +++ b/parser/testdata/InsertStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 90ad1a1bb5e713c8a2fed519d101d8eb003dbac3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:35:38 +0000 Subject: [PATCH 06/29] Add comprehensive CREATE EVENT SESSION parsing support - Update parseCreateEventSessionStatementFromEvent to fully parse the statement - Add JSON marshalling for EventDeclaration, TargetDeclaration, SessionOption types - Add EventDeclarationCompareFunctionParameter as BooleanExpression for predicate parsing - Add SourceDeclaration as both ScalarExpression and BooleanExpression - Add sourceDeclarationToJSON helper function - Enable 7 CreateEventSessionNotLikePredicate tests --- ast/event_statements.go | 95 ++++- ast/server_audit_statement.go | 5 +- parser/marshal.go | 166 ++++++++ parser/parse_statements.go | 395 +++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- 11 files changed, 645 insertions(+), 30 deletions(-) diff --git a/ast/event_statements.go b/ast/event_statements.go index d2751cff..e1dd67c5 100644 --- a/ast/event_statements.go +++ b/ast/event_statements.go @@ -2,11 +2,11 @@ package ast // CreateEventSessionStatement represents CREATE EVENT SESSION statement type CreateEventSessionStatement struct { - Name *Identifier - ServerName *Identifier - Events []*EventDeclaration - Targets []*EventTarget - Options []*EventSessionOption + Name *Identifier + SessionScope string // "Server" or "Database" + EventDeclarations []*EventDeclaration + TargetDeclarations []*TargetDeclaration + SessionOptions []SessionOption } func (s *CreateEventSessionStatement) node() {} @@ -14,32 +14,101 @@ func (s *CreateEventSessionStatement) statement() {} // EventDeclaration represents an event in the event session type EventDeclaration struct { - PackageName *Identifier - EventName *Identifier - Actions []*EventAction - WhereClause ScalarExpression + ObjectName *EventSessionObjectName + EventDeclarationActionParameters []*EventSessionObjectName + EventDeclarationPredicateParameter BooleanExpression } -// EventAction represents an action for an event +// Note: EventSessionObjectName is defined in server_audit_statement.go + +// TargetDeclaration represents a target for the event session +type TargetDeclaration struct { + ObjectName *EventSessionObjectName + TargetDeclarationParameters []*EventDeclarationSetParameter +} + +// EventDeclarationSetParameter represents a SET parameter +type EventDeclarationSetParameter struct { + EventField *Identifier + EventValue ScalarExpression +} + +// SessionOption interface for event session options +type SessionOption interface { + sessionOption() +} + +// LiteralSessionOption represents a literal session option like MAX_MEMORY +type LiteralSessionOption struct { + OptionKind string + Value ScalarExpression + Unit string +} + +func (o *LiteralSessionOption) sessionOption() {} + +// OnOffSessionOption represents an ON/OFF session option +type OnOffSessionOption struct { + OptionKind string + OptionState string // "On" or "Off" +} + +func (o *OnOffSessionOption) sessionOption() {} + +// EventRetentionSessionOption represents EVENT_RETENTION_MODE option +type EventRetentionSessionOption struct { + OptionKind string + Value string // e.g. "AllowSingleEventLoss" +} + +func (o *EventRetentionSessionOption) sessionOption() {} + +// MaxDispatchLatencySessionOption represents MAX_DISPATCH_LATENCY option +type MaxDispatchLatencySessionOption struct { + OptionKind string + Value ScalarExpression + IsInfinite bool +} + +func (o *MaxDispatchLatencySessionOption) sessionOption() {} + +// MemoryPartitionSessionOption represents MEMORY_PARTITION_MODE option +type MemoryPartitionSessionOption struct { + OptionKind string + Value string // e.g. "None" +} + +func (o *MemoryPartitionSessionOption) sessionOption() {} + +// EventDeclarationCompareFunctionParameter for function calls in WHERE clause +type EventDeclarationCompareFunctionParameter struct { + Name *EventSessionObjectName + SourceDeclaration *SourceDeclaration + EventValue ScalarExpression +} + +func (e *EventDeclarationCompareFunctionParameter) node() {} +func (e *EventDeclarationCompareFunctionParameter) booleanExpression() {} + +// Note: SourceDeclaration is defined in server_audit_statement.go + +// Legacy fields for backwards compatibility type EventAction struct { PackageName *Identifier ActionName *Identifier } -// EventTarget represents a target for the event session type EventTarget struct { PackageName *Identifier TargetName *Identifier Options []*EventTargetOption } -// EventTargetOption represents an option for an event target type EventTargetOption struct { Name *Identifier Value ScalarExpression } -// EventSessionOption represents an option for the event session type EventSessionOption struct { OptionKind string Value ScalarExpression diff --git a/ast/server_audit_statement.go b/ast/server_audit_statement.go index cf28fa2b..c396abb6 100644 --- a/ast/server_audit_statement.go +++ b/ast/server_audit_statement.go @@ -84,8 +84,9 @@ type SourceDeclaration struct { Value *EventSessionObjectName } -func (s *SourceDeclaration) node() {} -func (s *SourceDeclaration) scalarExpression() {} +func (s *SourceDeclaration) node() {} +func (s *SourceDeclaration) scalarExpression() {} +func (s *SourceDeclaration) booleanExpression() {} // EventSessionObjectName represents an event session object name type EventSessionObjectName struct { diff --git a/parser/marshal.go b/parser/marshal.go index 80cf3480..5cf462b4 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -1697,6 +1697,16 @@ func eventSessionObjectNameToJSON(e *ast.EventSessionObjectName) jsonNode { return node } +func sourceDeclarationToJSON(s *ast.SourceDeclaration) jsonNode { + node := jsonNode{ + "$type": "SourceDeclaration", + } + if s.Value != nil { + node["Value"] = eventSessionObjectNameToJSON(s.Value) + } + return node +} + func identifierOrValueExpressionToJSON(iove *ast.IdentifierOrValueExpression) jsonNode { node := jsonNode{ "$type": "IdentifierOrValueExpression", @@ -2036,6 +2046,22 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode { node["ThirdExpression"] = scalarExpressionToJSON(e.ThirdExpression) } return node + case *ast.EventDeclarationCompareFunctionParameter: + node := jsonNode{ + "$type": "EventDeclarationCompareFunctionParameter", + } + if e.Name != nil { + node["Name"] = eventSessionObjectNameToJSON(e.Name) + } + if e.SourceDeclaration != nil { + node["SourceDeclaration"] = sourceDeclarationToJSON(e.SourceDeclaration) + } + if e.EventValue != nil { + node["EventValue"] = scalarExpressionToJSON(e.EventValue) + } + return node + case *ast.SourceDeclaration: + return sourceDeclarationToJSON(e) default: return jsonNode{"$type": "UnknownBooleanExpression"} } @@ -10460,9 +10486,149 @@ func createEventSessionStatementToJSON(s *ast.CreateEventSessionStatement) jsonN if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.SessionScope != "" { + node["SessionScope"] = s.SessionScope + } + if len(s.EventDeclarations) > 0 { + events := make([]jsonNode, len(s.EventDeclarations)) + for i, e := range s.EventDeclarations { + events[i] = eventDeclarationToJSON(e) + } + node["EventDeclarations"] = events + } + if len(s.TargetDeclarations) > 0 { + targets := make([]jsonNode, len(s.TargetDeclarations)) + for i, t := range s.TargetDeclarations { + targets[i] = targetDeclarationToJSON(t) + } + node["TargetDeclarations"] = targets + } + if len(s.SessionOptions) > 0 { + opts := make([]jsonNode, len(s.SessionOptions)) + for i, o := range s.SessionOptions { + opts[i] = sessionOptionToJSON(o) + } + node["SessionOptions"] = opts + } + return node +} + +func eventDeclarationToJSON(e *ast.EventDeclaration) jsonNode { + node := jsonNode{ + "$type": "EventDeclaration", + } + if e.ObjectName != nil { + node["ObjectName"] = eventSessionObjectNameToJSON(e.ObjectName) + } + if len(e.EventDeclarationActionParameters) > 0 { + actions := make([]jsonNode, len(e.EventDeclarationActionParameters)) + for i, a := range e.EventDeclarationActionParameters { + actions[i] = eventSessionObjectNameToJSON(a) + } + node["EventDeclarationActionParameters"] = actions + } + if e.EventDeclarationPredicateParameter != nil { + node["EventDeclarationPredicateParameter"] = booleanExpressionToJSON(e.EventDeclarationPredicateParameter) + } + return node +} + +func targetDeclarationToJSON(t *ast.TargetDeclaration) jsonNode { + node := jsonNode{ + "$type": "TargetDeclaration", + } + if t.ObjectName != nil { + node["ObjectName"] = eventSessionObjectNameToJSON(t.ObjectName) + } + if len(t.TargetDeclarationParameters) > 0 { + params := make([]jsonNode, len(t.TargetDeclarationParameters)) + for i, p := range t.TargetDeclarationParameters { + params[i] = eventDeclarationSetParameterToJSON(p) + } + node["TargetDeclarationParameters"] = params + } + return node +} + +func eventDeclarationSetParameterToJSON(p *ast.EventDeclarationSetParameter) jsonNode { + node := jsonNode{ + "$type": "EventDeclarationSetParameter", + } + if p.EventField != nil { + node["EventField"] = identifierToJSON(p.EventField) + } + if p.EventValue != nil { + node["EventValue"] = scalarExpressionToJSON(p.EventValue) + } return node } +func sessionOptionToJSON(o ast.SessionOption) jsonNode { + switch opt := o.(type) { + case *ast.LiteralSessionOption: + node := jsonNode{ + "$type": "LiteralSessionOption", + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + if opt.Unit != "" { + node["Unit"] = opt.Unit + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.OnOffSessionOption: + node := jsonNode{ + "$type": "OnOffSessionOption", + } + if opt.OptionState != "" { + node["OptionState"] = opt.OptionState + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.EventRetentionSessionOption: + node := jsonNode{ + "$type": "EventRetentionSessionOption", + } + if opt.Value != "" { + node["Value"] = opt.Value + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.MaxDispatchLatencySessionOption: + node := jsonNode{ + "$type": "MaxDispatchLatencySessionOption", + "IsInfinite": opt.IsInfinite, + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.MemoryPartitionSessionOption: + node := jsonNode{ + "$type": "MemoryPartitionSessionOption", + } + if opt.Value != "" { + node["Value"] = opt.Value + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + default: + return jsonNode{"$type": "UnknownSessionOption"} + } +} + func insertBulkStatementToJSON(s *ast.InsertBulkStatement) jsonNode { node := jsonNode{ "$type": "InsertBulkStatement", diff --git a/parser/parse_statements.go b/parser/parse_statements.go index dbddf20a..59bd1918 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -6197,17 +6197,57 @@ func (p *Parser) parseCreateEventSessionStatement() (*ast.CreateEventSessionStat Name: p.parseIdentifier(), } - // ON SERVER + // ON SERVER/DATABASE if p.curTok.Type == TokenOn { p.nextToken() - if strings.ToUpper(p.curTok.Literal) == "SERVER" { + scopeUpper := strings.ToUpper(p.curTok.Literal) + if scopeUpper == "SERVER" { + stmt.SessionScope = "Server" + p.nextToken() + } else if scopeUpper == "DATABASE" { + stmt.SessionScope = "Database" p.nextToken() } } - // Skip rest of statement for now - event sessions are complex + // Parse ADD EVENT/TARGET and WITH clauses for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { - p.nextToken() + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "ADD" { + p.nextToken() + addType := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if addType == "EVENT" { + event := p.parseEventDeclaration() + stmt.EventDeclarations = append(stmt.EventDeclarations, event) + } else if addType == "TARGET" { + target := p.parseTargetDeclaration() + stmt.TargetDeclarations = append(stmt.TargetDeclarations, target) + } + } else if upperLit == "WITH" || p.curTok.Type == TokenWith { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + opt := p.parseSessionOption() + if opt != nil { + stmt.SessionOptions = append(stmt.SessionOptions, opt) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + p.nextToken() + } } if p.curTok.Type == TokenSemicolon { p.nextToken() @@ -6215,6 +6255,300 @@ func (p *Parser) parseCreateEventSessionStatement() (*ast.CreateEventSessionStat return stmt, nil } +func (p *Parser) parseEventDeclaration() *ast.EventDeclaration { + event := &ast.EventDeclaration{} + + // Parse package.event_name + event.ObjectName = p.parseEventSessionObjectName() + + // Parse optional ( ACTION(...) WHERE ... ) + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "ACTION" { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + actionName := p.parseEventSessionObjectName() + event.EventDeclarationActionParameters = append(event.EventDeclarationActionParameters, actionName) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else if upperLit == "WHERE" { + p.nextToken() + event.EventDeclarationPredicateParameter = p.parseEventPredicate() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return event +} + +func (p *Parser) parseTargetDeclaration() *ast.TargetDeclaration { + target := &ast.TargetDeclaration{} + + // Parse package.target_name + target.ObjectName = p.parseEventSessionObjectName() + + // Parse optional ( SET ... ) + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if strings.ToUpper(p.curTok.Literal) == "SET" { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + param := &ast.EventDeclarationSetParameter{ + EventField: p.parseIdentifier(), + } + if p.curTok.Type == TokenEquals { + p.nextToken() + param.EventValue, _ = p.parseScalarExpression() + } + target.TargetDeclarationParameters = append(target.TargetDeclarationParameters, param) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return target +} + +func (p *Parser) parseEventSessionObjectName() *ast.EventSessionObjectName { + var identifiers []*ast.Identifier + + for { + if p.curTok.Type != TokenIdent && p.curTok.Type != TokenLBracket { + break + } + identifiers = append(identifiers, p.parseIdentifier()) + if p.curTok.Type != TokenDot { + break + } + p.nextToken() // consume dot + } + + return &ast.EventSessionObjectName{ + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: identifiers, + Count: len(identifiers), + }, + } +} + +func (p *Parser) parseEventPredicate() ast.BooleanExpression { + return p.parseEventPredicateOr() +} + +func (p *Parser) parseEventPredicateOr() ast.BooleanExpression { + left := p.parseEventPredicateAnd() + for strings.ToUpper(p.curTok.Literal) == "OR" { + p.nextToken() + right := p.parseEventPredicateAnd() + left = &ast.BooleanBinaryExpression{ + BinaryExpressionType: "Or", + FirstExpression: left, + SecondExpression: right, + } + } + return left +} + +func (p *Parser) parseEventPredicateAnd() ast.BooleanExpression { + left := p.parseEventPredicatePrimary() + for strings.ToUpper(p.curTok.Literal) == "AND" { + p.nextToken() + right := p.parseEventPredicatePrimary() + left = &ast.BooleanBinaryExpression{ + BinaryExpressionType: "And", + FirstExpression: left, + SecondExpression: right, + } + } + return left +} + +func (p *Parser) parseEventPredicatePrimary() ast.BooleanExpression { + // Handle parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() + expr := p.parseEventPredicateOr() + if p.curTok.Type == TokenRParen { + p.nextToken() + } + return &ast.BooleanParenthesisExpression{Expression: expr} + } + + // Parse [package].[function_or_field](...) or [package].[field] NOT LIKE 'pattern' + name := p.parseEventSessionObjectName() + + // Check for function call + if p.curTok.Type == TokenLParen { + p.nextToken() + // Parse function parameters + var source *ast.SourceDeclaration + var eventValue ast.ScalarExpression + + // First param is usually a source declaration + sourceName := p.parseEventSessionObjectName() + source = &ast.SourceDeclaration{Value: sourceName} + + if p.curTok.Type == TokenComma { + p.nextToken() + eventValue, _ = p.parseScalarExpression() + } + + if p.curTok.Type == TokenRParen { + p.nextToken() + } + + return &ast.EventDeclarationCompareFunctionParameter{ + Name: name, + SourceDeclaration: source, + EventValue: eventValue, + } + } + + // Check for NOT LIKE or LIKE + notLike := false + if strings.ToUpper(p.curTok.Literal) == "NOT" { + notLike = true + p.nextToken() + } + + if strings.ToUpper(p.curTok.Literal) == "LIKE" { + p.nextToken() + pattern, _ := p.parseScalarExpression() + compType := "Like" + if notLike { + compType = "NotLike" + } + return &ast.BooleanComparisonExpression{ + ComparisonType: compType, + FirstExpression: &ast.SourceDeclaration{Value: name}, + SecondExpression: pattern, + } + } + + // Fallback: return source declaration wrapped in something + return &ast.SourceDeclaration{Value: name} +} + +func (p *Parser) parseSessionOption() ast.SessionOption { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + switch optName { + case "MAX_MEMORY", "MAX_EVENT_SIZE": + value, _ := p.parseScalarExpression() + unit := "" + if strings.ToUpper(p.curTok.Literal) == "KB" || strings.ToUpper(p.curTok.Literal) == "MB" { + unit = strings.ToUpper(p.curTok.Literal) + p.nextToken() + } + return &ast.LiteralSessionOption{ + OptionKind: p.sessionOptionKind(optName), + Value: value, + Unit: unit, + } + case "EVENT_RETENTION_MODE": + value := p.curTok.Literal + p.nextToken() + return &ast.EventRetentionSessionOption{ + OptionKind: "EventRetention", + Value: p.eventRetentionValue(value), + } + case "MAX_DISPATCH_LATENCY": + value, _ := p.parseScalarExpression() + // Check for SECONDS + if strings.ToUpper(p.curTok.Literal) == "SECONDS" { + p.nextToken() + } + return &ast.MaxDispatchLatencySessionOption{ + OptionKind: "MaxDispatchLatency", + Value: value, + IsInfinite: false, + } + case "MEMORY_PARTITION_MODE": + value := p.curTok.Literal + p.nextToken() + return &ast.MemoryPartitionSessionOption{ + OptionKind: "MemoryPartition", + Value: p.capitalizeFirst(strings.ToLower(value)), + } + case "TRACK_CAUSALITY", "STARTUP_STATE": + stateUpper := strings.ToUpper(p.curTok.Literal) + p.nextToken() + state := "Off" + if stateUpper == "ON" { + state = "On" + } + return &ast.OnOffSessionOption{ + OptionKind: p.sessionOptionKind(optName), + OptionState: state, + } + default: + // Skip unknown option value + p.nextToken() + return nil + } +} + +func (p *Parser) sessionOptionKind(name string) string { + switch name { + case "MAX_MEMORY": + return "MaxMemory" + case "MAX_EVENT_SIZE": + return "MaxEventSize" + case "TRACK_CAUSALITY": + return "TrackCausality" + case "STARTUP_STATE": + return "StartUpState" + default: + return name + } +} + +func (p *Parser) eventRetentionValue(value string) string { + switch strings.ToUpper(value) { + case "ALLOW_SINGLE_EVENT_LOSS": + return "AllowSingleEventLoss" + case "ALLOW_MULTIPLE_EVENT_LOSS": + return "AllowMultipleEventLoss" + case "NO_EVENT_LOSS": + return "NoEventLoss" + default: + return value + } +} + func (p *Parser) parseCreateEventSessionStatementFromEvent() (*ast.CreateEventSessionStatement, error) { // EVENT has already been consumed, curTok is SESSION p.nextToken() // consume SESSION @@ -6223,16 +6557,61 @@ func (p *Parser) parseCreateEventSessionStatementFromEvent() (*ast.CreateEventSe Name: p.parseIdentifier(), } - // ON SERVER + // ON SERVER/DATABASE if p.curTok.Type == TokenOn { p.nextToken() - if strings.ToUpper(p.curTok.Literal) == "SERVER" { + scopeUpper := strings.ToUpper(p.curTok.Literal) + if scopeUpper == "SERVER" { + stmt.SessionScope = "Server" + p.nextToken() + } else if scopeUpper == "DATABASE" { + stmt.SessionScope = "Database" p.nextToken() } } - // Skip rest of statement - p.skipToEndOfStatement() + // Parse ADD EVENT/TARGET and WITH clauses + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "ADD" { + p.nextToken() + addType := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if addType == "EVENT" { + event := p.parseEventDeclaration() + stmt.EventDeclarations = append(stmt.EventDeclarations, event) + } else if addType == "TARGET" { + target := p.parseTargetDeclaration() + stmt.TargetDeclarations = append(stmt.TargetDeclarations, target) + } + } else if upperLit == "WITH" || p.curTok.Type == TokenWith { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + opt := p.parseSessionOption() + if opt != nil { + stmt.SessionOptions = append(stmt.SessionOptions, opt) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + p.nextToken() + } + } + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } return stmt, nil } diff --git a/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From c20a50eefe9d2a97920ddf3f356e6b44ed250c42 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:39:15 +0000 Subject: [PATCH 07/29] Add PARTITION BY support in OVER clause for window functions - Add Partitions and OrderByClause fields to OverClause AST type - Update OVER clause parser to handle PARTITION BY and ORDER BY - Add overClauseToJSON function for proper marshalling - Enable 4 WithinGroupTests (130, 140, both baseline and regular) --- ast/function_call.go | 3 +- parser/marshal.go | 21 +++++++++-- parser/parse_select.go | 36 +++++++++++++++++-- .../metadata.json | 2 +- .../metadata.json | 2 +- .../WithinGroupTests130/metadata.json | 2 +- .../WithinGroupTests140/metadata.json | 2 +- 7 files changed, 57 insertions(+), 11 deletions(-) diff --git a/ast/function_call.go b/ast/function_call.go index f55bdcdf..1d3836ea 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -28,7 +28,8 @@ func (*UserDefinedTypeCallTarget) callTarget() {} // OverClause represents an OVER clause for window functions. type OverClause struct { - // Add partition by, order by, and window frame as needed + Partitions []ScalarExpression `json:"Partitions,omitempty"` + OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` } // WithinGroupClause represents a WITHIN GROUP clause for ordered set aggregate functions. diff --git a/parser/marshal.go b/parser/marshal.go index 5cf462b4..13eb605f 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -1389,9 +1389,7 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["WithinGroupClause"] = withinGroupClauseToJSON(e.WithinGroupClause) } if e.OverClause != nil { - node["OverClause"] = jsonNode{ - "$type": "OverClause", - } + node["OverClause"] = overClauseToJSON(e.OverClause) } if len(e.IgnoreRespectNulls) > 0 { idents := make([]jsonNode, len(e.IgnoreRespectNulls)) @@ -2151,6 +2149,23 @@ func withinGroupClauseToJSON(wg *ast.WithinGroupClause) jsonNode { return node } +func overClauseToJSON(oc *ast.OverClause) jsonNode { + node := jsonNode{ + "$type": "OverClause", + } + if len(oc.Partitions) > 0 { + partitions := make([]jsonNode, len(oc.Partitions)) + for i, p := range oc.Partitions { + partitions[i] = scalarExpressionToJSON(p) + } + node["Partitions"] = partitions + } + if oc.OrderByClause != nil { + node["OrderByClause"] = orderByClauseToJSON(oc.OrderByClause) + } + return node +} + // ======================= New Statement JSON Functions ======================= func tableHintToJSON(h ast.TableHintType) jsonNode { diff --git a/parser/parse_select.go b/parser/parse_select.go index a0c04f6a..4e0fa249 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -1374,14 +1374,44 @@ func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.Scala } p.nextToken() // consume ( - // For now, just skip to closing paren (basic OVER() support) - // TODO: Parse partition by, order by, and window frame + overClause := &ast.OverClause{} + + // Parse PARTITION BY + if strings.ToUpper(p.curTok.Literal) == "PARTITION" { + p.nextToken() // consume PARTITION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + // Parse partition expressions + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + partExpr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + overClause.Partitions = append(overClause.Partitions, partExpr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + + // Parse ORDER BY + if p.curTok.Type == TokenOrder { + orderBy, err := p.parseOrderByClause() + if err != nil { + return nil, err + } + overClause.OrderByClause = orderBy + } + if p.curTok.Type != TokenRParen { return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) } p.nextToken() // consume ) - fc.OverClause = &ast.OverClause{} + fc.OverClause = overClause } break diff --git a/parser/testdata/Baselines130_WithinGroupTests130/metadata.json b/parser/testdata/Baselines130_WithinGroupTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_WithinGroupTests130/metadata.json +++ b/parser/testdata/Baselines130_WithinGroupTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_WithinGroupTests140/metadata.json b/parser/testdata/Baselines140_WithinGroupTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_WithinGroupTests140/metadata.json +++ b/parser/testdata/Baselines140_WithinGroupTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/WithinGroupTests130/metadata.json b/parser/testdata/WithinGroupTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WithinGroupTests130/metadata.json +++ b/parser/testdata/WithinGroupTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/WithinGroupTests140/metadata.json b/parser/testdata/WithinGroupTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WithinGroupTests140/metadata.json +++ b/parser/testdata/WithinGroupTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 112028cc0fa063edcbdb201074583f22bde1f9f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:41:34 +0000 Subject: [PATCH 08/29] Fix CreateFulltextCatalogStatementTests parsing - Convert query.sql from UTF-16 to UTF-8 encoding - Add isStatementTerminator() check to parser loop to properly separate statements without semicolons --- parser/parse_statements.go | 2 +- .../metadata.json | 2 +- .../query.sql | Bin 586 -> 295 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 59bd1918..b0f1a034 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -8935,7 +8935,7 @@ func (p *Parser) parseCreateFulltextCatalogStatement() (*ast.CreateFullTextCatal } // Parse optional clauses - for p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon && !p.isBatchSeparator() { + for p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon && !p.isBatchSeparator() && !p.isStatementTerminator() { switch strings.ToUpper(p.curTok.Literal) { case "ON": p.nextToken() // consume ON diff --git a/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json b/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json +++ b/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateFulltextCatalogStatementTests/query.sql b/parser/testdata/CreateFulltextCatalogStatementTests/query.sql index b8047d11958edb2489ac58c24b3b7d7b09549add..5b1227e7d403f215dd6a7f5217f9a9e7f4ed6e1f 100644 GIT binary patch literal 295 zcmbV{yA8uI5JP(d-r+je86dL+2jxyaK#>fI%15iE$Vgcs$^cg3jsVF=@caI_EDRy7 zAwZrW$Ka4WxzjxDdz2!n1@Sr6jnt3B7HMOww3Kv{zgmXuhxIHwClQw-u^8-bc+t79 b9xbX87rKga4H!bWVoli^!}`hA+C2RLsVr+_ literal 586 zcmbu6T@HdU5QOL1#5?qjXAj_645<{GC=d$%9bWu)6AvJy>C&Cj*_qwt6E&()t%(?v zH0#Q;>`4rYepiF_YCS(_uO0aR?*bg1SIO`-@Qom|2FF@$Vb0DxxY`SRl?vQ9s{F8O zgYJhi-7wx7=MmP9{6q~q6~E28`}f6PN49xg->G#K6B9Izg?L6=;%pP0HYhO$^WHP9 PthaYKySqSU%Upf|RP1W^ From 070ed3e3b90a9a6d9ba71ac396929828e5e62f64 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:42:57 +0000 Subject: [PATCH 09/29] Fix Baselines160_CreateProcedureStatementTests160 encoding - Convert query.sql from UTF-16 to UTF-8 encoding --- .../metadata.json | 2 +- .../query.sql | Bin 592 -> 298 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json b/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql b/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql index 179241b196e66b0511b20d586f5eeeb87adcc4b0..67cce62bce9e0cca111d3d9006ba2db4eeae71e5 100644 GIT binary patch literal 298 zcmaFAd%ts#t7C|(LO_tev#U#JkgGy;N>YBTUUX1to>y^xURYvIX=*GlmqS5fQDUy4 zf>*G=A1{|^;x{6++x~4TR7YMrh^K#*_BrG+#B)Bz0OG=CKus8u?ZIEk7XpkSsaBBed9aMY( literal 592 zcmb`FL2JT55QX2l(EpG_K?NzqYcG)~qLf%P=2A*XV-A9*MXNu*?Kf)-K|F|fn4Q^~ z$D4Vx`}Z4arlI Date: Thu, 1 Jan 2026 08:50:40 +0000 Subject: [PATCH 10/29] Add COLUMNSTORE index parsing improvements - Add OnFileGroupOrPartitionScheme field to CreateColumnStoreIndexStatement - Parse DROP_EXISTING and MAXDOP index options - Add IndexExpressionOption marshalling to columnStoreIndexOptionToJSON - Parse ON filegroup/partition scheme clause for COLUMNSTORE indexes Enables 2 tests: CreateIndexStatementTests110, Baselines110_CreateIndexStatementTests110 --- ast/create_columnstore_index_statement.go | 19 +++--- parser/marshal.go | 64 ++++++++++++++++--- .../metadata.json | 2 +- .../metadata.json | 2 +- 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/ast/create_columnstore_index_statement.go b/ast/create_columnstore_index_statement.go index e752d0fc..42e8d705 100644 --- a/ast/create_columnstore_index_statement.go +++ b/ast/create_columnstore_index_statement.go @@ -2,15 +2,16 @@ package ast // CreateColumnStoreIndexStatement represents a CREATE COLUMNSTORE INDEX statement type CreateColumnStoreIndexStatement struct { - Name *Identifier - Clustered bool - ClusteredExplicit bool // true if CLUSTERED or NONCLUSTERED was explicitly specified - OnName *SchemaObjectName - Columns []*ColumnReferenceExpression - OrderedColumns []*ColumnReferenceExpression - IndexOptions []IndexOption - FilterClause BooleanExpression - OnPartition *PartitionSpecifier + Name *Identifier + Clustered bool + ClusteredExplicit bool // true if CLUSTERED or NONCLUSTERED was explicitly specified + OnName *SchemaObjectName + Columns []*ColumnReferenceExpression + OrderedColumns []*ColumnReferenceExpression + IndexOptions []IndexOption + FilterClause BooleanExpression + OnPartition *PartitionSpecifier + OnFileGroupOrPartitionScheme *FileGroupOrPartitionScheme } func (s *CreateColumnStoreIndexStatement) statement() {} diff --git a/parser/marshal.go b/parser/marshal.go index 13eb605f..bfa97c0d 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -7059,8 +7059,12 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI } stmt.IndexOptions = append(stmt.IndexOptions, opt) - case "SORT_IN_TEMPDB": - p.nextToken() // consume SORT_IN_TEMPDB + case "SORT_IN_TEMPDB", "DROP_EXISTING": + optKind := "SortInTempDB" + if optName == "DROP_EXISTING" { + optKind = "DropExisting" + } + p.nextToken() // consume option name if p.curTok.Type == TokenEquals { p.nextToken() // consume = } @@ -7073,10 +7077,24 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI p.nextToken() } stmt.IndexOptions = append(stmt.IndexOptions, &ast.IndexStateOption{ - OptionKind: "SortInTempDB", + OptionKind: optKind, OptionState: state, }) + case "MAXDOP": + p.nextToken() // consume MAXDOP + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.IndexOptions = append(stmt.IndexOptions, &ast.IndexExpressionOption{ + OptionKind: "MaxDop", + Expression: expr, + }) + case "ORDER": p.nextToken() // consume ORDER if p.curTok.Type == TokenLParen { @@ -7121,13 +7139,31 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI } } - // Skip optional ON partition clause + // Parse optional ON filegroup/partition scheme if p.curTok.Type == TokenOn { - p.nextToken() - // Skip to semicolon - for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { - p.nextToken() + p.nextToken() // consume ON + fgps := &ast.FileGroupOrPartitionScheme{ + Name: &ast.IdentifierOrValueExpression{ + Identifier: p.parseIdentifier(), + }, + } + fgps.Name.Value = fgps.Name.Identifier.Value + // Check for partition columns + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + fgps.PartitionSchemeColumns = append(fgps.PartitionSchemeColumns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } } + stmt.OnFileGroupOrPartitionScheme = fgps } // Skip optional semicolon @@ -8893,6 +8929,9 @@ func createColumnStoreIndexStatementToJSON(s *ast.CreateColumnStoreIndexStatemen } node["OrderedColumns"] = cols } + if s.OnFileGroupOrPartitionScheme != nil { + node["OnFileGroupOrPartitionScheme"] = fileGroupOrPartitionSchemeToJSON(s.OnFileGroupOrPartitionScheme) + } return node } @@ -8927,6 +8966,15 @@ func columnStoreIndexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionKind": o.OptionKind, "OptionState": o.OptionState, } + case *ast.IndexExpressionOption: + node := jsonNode{ + "$type": "IndexExpressionOption", + "OptionKind": o.OptionKind, + } + if o.Expression != nil { + node["Expression"] = scalarExpressionToJSON(o.Expression) + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } diff --git a/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json b/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateIndexStatementTests110/metadata.json b/parser/testdata/CreateIndexStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests110/metadata.json +++ b/parser/testdata/CreateIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 0a9ea05756cb3de45350f29760b88f6407867d71 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 08:59:11 +0000 Subject: [PATCH 11/29] Add END CONVERSATION statement parsing - Create EndConversationStatement AST type with Conversation, WithCleanup, ErrorCode, and ErrorDescription fields - Add parseEndConversationStatement function to handle END CONVERSATION with optional WITH ERROR/WITH CLEANUP clauses - Update TryCatch, BeginEndBlock, and statement list parsing to properly distinguish END CONVERSATION from block-terminating END keyword - Add JSON marshalling for EndConversationStatement Enables 2 tests: EndConversationStatementTests, Baselines90_EndConversationStatementTests --- ast/end_conversation_statement.go | 12 ++ parser/marshal.go | 19 +++ parser/parse_statements.go | 160 +++++++++++++++++- parser/parser.go | 6 + .../metadata.json | 2 +- .../metadata.json | 2 +- 6 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 ast/end_conversation_statement.go diff --git a/ast/end_conversation_statement.go b/ast/end_conversation_statement.go new file mode 100644 index 00000000..c0bb123b --- /dev/null +++ b/ast/end_conversation_statement.go @@ -0,0 +1,12 @@ +package ast + +// EndConversationStatement represents END CONVERSATION statement +type EndConversationStatement struct { + Conversation ScalarExpression // The conversation handle + WithCleanup bool // true if WITH CLEANUP specified + ErrorCode ScalarExpression // optional error code with WITH ERROR + ErrorDescription ScalarExpression // optional error description with WITH ERROR +} + +func (s *EndConversationStatement) statement() {} +func (s *EndConversationStatement) node() {} diff --git a/parser/marshal.go b/parser/marshal.go index bfa97c0d..98354151 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -378,6 +378,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return createTriggerStatementToJSON(s) case *ast.EnableDisableTriggerStatement: return enableDisableTriggerStatementToJSON(s) + case *ast.EndConversationStatement: + return endConversationStatementToJSON(s) case *ast.CreateDatabaseStatement: return createDatabaseStatementToJSON(s) case *ast.CreateDatabaseEncryptionKeyStatement: @@ -9395,6 +9397,23 @@ func enableDisableTriggerStatementToJSON(s *ast.EnableDisableTriggerStatement) j return node } +func endConversationStatementToJSON(s *ast.EndConversationStatement) jsonNode { + node := jsonNode{ + "$type": "EndConversationStatement", + "WithCleanup": s.WithCleanup, + } + if s.Conversation != nil { + node["Conversation"] = scalarExpressionToJSON(s.Conversation) + } + if s.ErrorCode != nil { + node["ErrorCode"] = scalarExpressionToJSON(s.ErrorCode) + } + if s.ErrorDescription != nil { + node["ErrorDescription"] = scalarExpressionToJSON(s.ErrorDescription) + } + return node +} + func alterIndexStatementToJSON(s *ast.AlterIndexStatement) jsonNode { node := jsonNode{ "$type": "AlterIndexStatement", diff --git a/parser/parse_statements.go b/parser/parse_statements.go index b0f1a034..cd8179c4 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -1518,12 +1518,28 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { } // Parse statements until END TRY - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + for p.curTok.Type != TokenEOF { // Skip semicolons if p.curTok.Type == TokenSemicolon { p.nextToken() continue } + // Check for END TRY (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.TryStatements.Statements = append(stmt.TryStatements.Statements, endConvStmt) + } + continue + } + // It's END TRY, break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1552,12 +1568,28 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { stmt.CatchStatements = &ast.StatementList{} // Parse catch statements until END CATCH - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + for p.curTok.Type != TokenEOF { // Skip semicolons if p.curTok.Type == TokenSemicolon { p.nextToken() continue } + // Check for END CATCH (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.CatchStatements.Statements = append(stmt.CatchStatements.Statements, endConvStmt) + } + continue + } + // It's END CATCH, break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1588,8 +1620,24 @@ func (p *Parser) parseBeginEndBlockStatementContinued() (*ast.BeginEndBlockState StatementList: &ast.StatementList{}, } - // Parse statements until END - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + // Parse statements until END (but not END CONVERSATION) + for p.curTok.Type != TokenEOF { + // Check for END (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.StatementList.Statements = append(stmt.StatementList.Statements, endConvStmt) + } + continue + } + // It's END (block terminator), break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1620,8 +1668,24 @@ func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, err StatementList: &ast.StatementList{}, } - // Parse statements until END - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + // Parse statements until END (but not END CONVERSATION) + for p.curTok.Type != TokenEOF { + // Check for END (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.StatementList.Statements = append(stmt.StatementList.Statements, endConvStmt) + } + continue + } + // It's END (block terminator), break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -2845,8 +2909,21 @@ func (p *Parser) parseStatementList() (*ast.StatementList, error) { continue } - // Check for END (end of BEGIN block or TRY/CATCH) + // Check for END (end of BEGIN block or TRY/CATCH, or END CONVERSATION statement) if p.curTok.Type == TokenEnd { + // Look ahead to check if it's END CONVERSATION (a statement) + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION statement, parse it + stmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if stmt != nil { + sl.Statements = append(sl.Statements, stmt) + } + continue + } + // Otherwise it's the end of a BEGIN block break } @@ -9840,6 +9917,75 @@ func (p *Parser) parseEnableDisableTriggerStatement(enforcement string) (*ast.En return stmt, nil } +// parseEndConversationStatement parses END CONVERSATION statements +func (p *Parser) parseEndConversationStatement() (*ast.EndConversationStatement, error) { + // Consume END + p.nextToken() + + // Expect CONVERSATION + if strings.ToUpper(p.curTok.Literal) != "CONVERSATION" { + return nil, fmt.Errorf("expected CONVERSATION after END, got %s", p.curTok.Literal) + } + p.nextToken() + + stmt := &ast.EndConversationStatement{} + + // Parse the conversation handle expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Conversation = expr + + // Check for WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() + + if strings.ToUpper(p.curTok.Literal) == "CLEANUP" { + stmt.WithCleanup = true + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "ERROR" { + p.nextToken() + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + // Parse error code + errCode, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.ErrorCode = errCode + + // Expect DESCRIPTION + if strings.ToUpper(p.curTok.Literal) == "DESCRIPTION" { + p.nextToken() + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + // Parse error description + errDesc, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.ErrorDescription = errDesc + } + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + // parseCreateWorkloadGroupStatement parses CREATE WORKLOAD GROUP statement. func (p *Parser) parseCreateWorkloadGroupStatement() (*ast.CreateWorkloadGroupStatement, error) { // Consume WORKLOAD diff --git a/parser/parser.go b/parser/parser.go index 3187e3c5..a572618f 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -181,6 +181,12 @@ func (p *Parser) parseStatement() (ast.Statement, error) { return p.parseBackupStatement() case TokenClose: return p.parseCloseStatement() + case TokenEnd: + // Check for END CONVERSATION + if p.peekTok.Type == TokenConversation { + return p.parseEndConversationStatement() + } + return nil, fmt.Errorf("unexpected token: END") case TokenOpen: return p.parseOpenStatement() case TokenDbcc: diff --git a/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json b/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json +++ b/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/EndConversationStatementTests/metadata.json b/parser/testdata/EndConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/EndConversationStatementTests/metadata.json +++ b/parser/testdata/EndConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 4fcb35efcb8a78cd075508272fef07807d990e75 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:09:50 +0000 Subject: [PATCH 12/29] Add COLLATE clause support for expressions - Add Collation field to FunctionCall and ColumnReferenceExpression AST types - Parse COLLATE clause after expressions in SELECT elements - Add COLLATE to excluded alias keywords to prevent misinterpretation - Update marshalling for both FunctionCall and ColumnReferenceExpression to include Collation field in JSON output Enables 4 tests: SelectWithCollation, Baselines110/120/130_SelectWithCollation --- ast/column_reference_expression.go | 1 + ast/function_call.go | 1 + parser/marshal.go | 9 +++++++++ parser/parse_select.go | 15 ++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- parser/testdata/SelectWithCollation/metadata.json | 2 +- 8 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ast/column_reference_expression.go b/ast/column_reference_expression.go index e4db7e6c..d666700a 100644 --- a/ast/column_reference_expression.go +++ b/ast/column_reference_expression.go @@ -4,6 +4,7 @@ package ast type ColumnReferenceExpression struct { ColumnType string `json:"ColumnType,omitempty"` MultiPartIdentifier *MultiPartIdentifier `json:"MultiPartIdentifier,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } func (*ColumnReferenceExpression) node() {} diff --git a/ast/function_call.go b/ast/function_call.go index 1d3836ea..67b113e1 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -50,6 +50,7 @@ type FunctionCall struct { OverClause *OverClause `json:"OverClause,omitempty"` IgnoreRespectNulls []*Identifier `json:"IgnoreRespectNulls,omitempty"` WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } func (*FunctionCall) node() {} diff --git a/parser/marshal.go b/parser/marshal.go index 98354151..a06daf6e 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -1316,6 +1316,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { if e.MultiPartIdentifier != nil { node["MultiPartIdentifier"] = multiPartIdentifierToJSON(e.MultiPartIdentifier) } + if e.Collation != nil { + node["Collation"] = identifierToJSON(e.Collation) + } return node case *ast.IntegerLiteral: node := jsonNode{ @@ -1401,6 +1404,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["IgnoreRespectNulls"] = idents } node["WithArrayWrapper"] = e.WithArrayWrapper + if e.Collation != nil { + node["Collation"] = identifierToJSON(e.Collation) + } return node case *ast.UserDefinedTypePropertyAccess: node := jsonNode{ @@ -5510,6 +5516,9 @@ func columnReferenceExpressionToJSON(c *ast.ColumnReferenceExpression) jsonNode if c.MultiPartIdentifier != nil { node["MultiPartIdentifier"] = multiPartIdentifierToJSON(c.MultiPartIdentifier) } + if c.Collation != nil { + node["Collation"] = identifierToJSON(c.Collation) + } return node } diff --git a/parser/parse_select.go b/parser/parse_select.go index 4e0fa249..2628d8c6 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -479,6 +479,19 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } } + // Check for COLLATE clause before creating SelectScalarExpression + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + collation := p.parseIdentifier() + // Attach collation to the expression + switch e := expr.(type) { + case *ast.FunctionCall: + e.Collation = collation + case *ast.ColumnReferenceExpression: + e.Collation = collation + } + } + sse := &ast.SelectScalarExpression{Expression: expr} // Check for column alias: [alias], AS alias, or just alias @@ -509,7 +522,7 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } else if p.curTok.Type == TokenIdent { // Check if this is an alias (not a keyword that starts a new clause) upper := strings.ToUpper(p.curTok.Literal) - if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" { + if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" && upper != "COLLATE" { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ Value: alias.Value, diff --git a/parser/testdata/Baselines110_SelectWithCollation/metadata.json b/parser/testdata/Baselines110_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines110_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_SelectWithCollation/metadata.json b/parser/testdata/Baselines120_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines120_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_SelectWithCollation/metadata.json b/parser/testdata/Baselines130_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines130_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/SelectWithCollation/metadata.json b/parser/testdata/SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/SelectWithCollation/metadata.json +++ b/parser/testdata/SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 66f9ed5b35f8d2268ee0725c39476b995ea8722e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:13:10 +0000 Subject: [PATCH 13/29] Add SPARSE/FILESTREAM column storage options parsing - Add ColumnStorageOptions struct to AST with IsFileStream and SparseOption - Add StorageOptions field to ColumnDefinition - Parse SPARSE, FILESTREAM, COLUMN_SET FOR ALL_SPARSE_COLUMNS in column definitions - Also parse ROWGUIDCOL, HIDDEN, MASKED column attributes - Add columnStorageOptionsToJSON for marshalling Enables 2 tests: ColumnDefinitionTests100, Baselines100_ColumnDefinitionTests100 --- ast/create_table_statement.go | 9 +++ parser/marshal.go | 65 +++++++++++++++++++ .../metadata.json | 2 +- .../ColumnDefinitionTests100/metadata.json | 2 +- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/ast/create_table_statement.go b/ast/create_table_statement.go index 6699e2ec..22b314ea 100644 --- a/ast/create_table_statement.go +++ b/ast/create_table_statement.go @@ -49,10 +49,19 @@ type ColumnDefinition struct { IsHidden bool IsMasked bool Nullable *NullableConstraintDefinition + StorageOptions *ColumnStorageOptions } func (c *ColumnDefinition) node() {} +// ColumnStorageOptions represents storage options for a column (SPARSE, FILESTREAM) +type ColumnStorageOptions struct { + IsFileStream bool // true if FILESTREAM specified + SparseOption string // "None", "Sparse", "ColumnSetForAllSparseColumns" +} + +func (c *ColumnStorageOptions) node() {} + // DataTypeReference is an interface for data type references type DataTypeReference interface { Node diff --git a/parser/marshal.go b/parser/marshal.go index a06daf6e..14a08fa9 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -3570,6 +3570,56 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { indexDef.Name = p.parseIdentifier() } col.Index = indexDef + } else if upperLit == "SPARSE" { + p.nextToken() // consume SPARSE + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.SparseOption = "Sparse" + } else if upperLit == "FILESTREAM" { + p.nextToken() // consume FILESTREAM + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.IsFileStream = true + } else if upperLit == "COLUMN_SET" { + p.nextToken() // consume COLUMN_SET + // Expect FOR ALL_SPARSE_COLUMNS + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + if strings.ToUpper(p.curTok.Literal) == "ALL_SPARSE_COLUMNS" { + p.nextToken() // consume ALL_SPARSE_COLUMNS + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.SparseOption = "ColumnSetForAllSparseColumns" + } + } + } else if upperLit == "ROWGUIDCOL" { + p.nextToken() // consume ROWGUIDCOL + col.IsRowGuidCol = true + } else if upperLit == "HIDDEN" { + p.nextToken() // consume HIDDEN + col.IsHidden = true + } else if upperLit == "MASKED" { + p.nextToken() // consume MASKED + col.IsMasked = true + // Skip optional WITH clause + if strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() + if p.curTok.Type == TokenLParen { + depth := 1 + p.nextToken() + for depth > 0 && p.curTok.Type != TokenEOF { + if p.curTok.Type == TokenLParen { + depth++ + } else if p.curTok.Type == TokenRParen { + depth-- + } + p.nextToken() + } + } + } } else { break } @@ -4847,6 +4897,9 @@ func columnDefinitionToJSON(c *ast.ColumnDefinition) jsonNode { "IsMasked": c.IsMasked, "ColumnIdentifier": identifierToJSON(c.ColumnIdentifier), } + if c.StorageOptions != nil { + node["StorageOptions"] = columnStorageOptionsToJSON(c.StorageOptions) + } if c.ComputedColumnExpression != nil { node["ComputedColumnExpression"] = scalarExpressionToJSON(c.ComputedColumnExpression) } @@ -4875,6 +4928,18 @@ func columnDefinitionToJSON(c *ast.ColumnDefinition) jsonNode { return node } +func columnStorageOptionsToJSON(o *ast.ColumnStorageOptions) jsonNode { + sparseOption := o.SparseOption + if sparseOption == "" { + sparseOption = "None" + } + return jsonNode{ + "$type": "ColumnStorageOptions", + "IsFileStream": o.IsFileStream, + "SparseOption": sparseOption, + } +} + func defaultConstraintToJSON(d *ast.DefaultConstraintDefinition) jsonNode { node := jsonNode{ "$type": "DefaultConstraintDefinition", diff --git a/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json b/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json +++ b/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ColumnDefinitionTests100/metadata.json b/parser/testdata/ColumnDefinitionTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ColumnDefinitionTests100/metadata.json +++ b/parser/testdata/ColumnDefinitionTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 099fe84d59b559667329fe5d2a264a332a1406e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:17:19 +0000 Subject: [PATCH 14/29] Add TOP and OUTPUT clause parsing to DELETE statement - Add TopRowFilter, OutputClause, OutputIntoClause fields to DeleteSpecification - Parse TOP (expression) [PERCENT] clause in DELETE statement - Parse OUTPUT and OUTPUT INTO clauses in DELETE statement - Update deleteSpecificationToJSON to marshal new fields Enables 2 tests: DeleteStatementTests90, Baselines90_DeleteStatementTests90 --- ast/delete_statement.go | 9 +++++--- parser/marshal.go | 9 ++++++++ parser/parse_dml.go | 23 +++++++++++++++++++ .../metadata.json | 2 +- .../DeleteStatementTests90/metadata.json | 2 +- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ast/delete_statement.go b/ast/delete_statement.go index 392e4647..d1a2b665 100644 --- a/ast/delete_statement.go +++ b/ast/delete_statement.go @@ -12,7 +12,10 @@ func (d *DeleteStatement) statement() {} // DeleteSpecification contains the details of a DELETE. type DeleteSpecification struct { - Target TableReference `json:"Target,omitempty"` - FromClause *FromClause `json:"FromClause,omitempty"` - WhereClause *WhereClause `json:"WhereClause,omitempty"` + Target TableReference `json:"Target,omitempty"` + FromClause *FromClause `json:"FromClause,omitempty"` + WhereClause *WhereClause `json:"WhereClause,omitempty"` + TopRowFilter *TopRowFilter `json:"TopRowFilter,omitempty"` + OutputClause *OutputClause `json:"OutputClause,omitempty"` + OutputIntoClause *OutputIntoClause `json:"OutputIntoClause,omitempty"` } diff --git a/parser/marshal.go b/parser/marshal.go index 14a08fa9..dc3a6d6d 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -2598,6 +2598,15 @@ func deleteSpecificationToJSON(spec *ast.DeleteSpecification) jsonNode { if spec.Target != nil { node["Target"] = tableReferenceToJSON(spec.Target) } + if spec.TopRowFilter != nil { + node["TopRowFilter"] = topRowFilterToJSON(spec.TopRowFilter) + } + if spec.OutputClause != nil { + node["OutputClause"] = outputClauseToJSON(spec.OutputClause) + } + if spec.OutputIntoClause != nil { + node["OutputIntoClause"] = outputIntoClauseToJSON(spec.OutputIntoClause) + } return node } diff --git a/parser/parse_dml.go b/parser/parse_dml.go index 507c47d0..00ce559f 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -1369,6 +1369,15 @@ func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) { DeleteSpecification: &ast.DeleteSpecification{}, } + // Parse optional TOP clause + if p.curTok.Type == TokenTop { + topRowFilter, err := p.parseTopRowFilter() + if err != nil { + return nil, err + } + stmt.DeleteSpecification.TopRowFilter = topRowFilter + } + // Skip optional FROM if p.curTok.Type == TokenFrom { p.nextToken() @@ -1381,6 +1390,20 @@ func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) { } stmt.DeleteSpecification.Target = target + // Parse OUTPUT clauses (can have OUTPUT INTO followed by OUTPUT) + for p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + outputClause, outputIntoClause, err := p.parseOutputClause() + if err != nil { + return nil, err + } + if outputIntoClause != nil { + stmt.DeleteSpecification.OutputIntoClause = outputIntoClause + } + if outputClause != nil { + stmt.DeleteSpecification.OutputClause = outputClause + } + } + // Parse optional FROM clause if p.curTok.Type == TokenFrom { fromClause, err := p.parseFromClause() diff --git a/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json b/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json +++ b/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DeleteStatementTests90/metadata.json b/parser/testdata/DeleteStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DeleteStatementTests90/metadata.json +++ b/parser/testdata/DeleteStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 8939ee06882805fa63bc576a232c92c81f0a5905 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:22:40 +0000 Subject: [PATCH 15/29] Add inline INDEX with INCLUDE clause parsing support Parse NONCLUSTERED/CLUSTERED index type, column list with ASC/DESC sort order, and INCLUDE clause for inline index definitions in CREATE TABLE column definitions. --- parser/marshal.go | 76 ++++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/parser/marshal.go b/parser/marshal.go index dc3a6d6d..222df3aa 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -3574,10 +3574,82 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { indexDef := &ast.IndexDefinition{ IndexType: &ast.IndexType{}, } - // Parse index name - if p.curTok.Type == TokenIdent { + // Parse index name (skip if it's CLUSTERED/NONCLUSTERED) + idxUpper := strings.ToUpper(p.curTok.Literal) + if p.curTok.Type == TokenIdent && idxUpper != "CLUSTERED" && idxUpper != "NONCLUSTERED" && p.curTok.Type != TokenLParen { indexDef.Name = p.parseIdentifier() } + // Parse optional CLUSTERED/NONCLUSTERED + if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { + indexDef.IndexType.IndexTypeKind = "Clustered" + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { + indexDef.IndexType.IndexTypeKind = "NonClustered" + p.nextToken() + } + // Parse optional column list: (col1 [ASC|DESC], ...) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colWithSort := &ast.ColumnWithSortOrder{ + SortOrder: ast.SortOrderNotSpecified, + } + // Parse column name + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{p.parseIdentifier()}, + }, + } + colRef.MultiPartIdentifier.Count = len(colRef.MultiPartIdentifier.Identifiers) + colWithSort.Column = colRef + + // Parse optional ASC/DESC + if strings.ToUpper(p.curTok.Literal) == "ASC" { + colWithSort.SortOrder = ast.SortOrderAscending + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "DESC" { + colWithSort.SortOrder = ast.SortOrderDescending + p.nextToken() + } + indexDef.Columns = append(indexDef.Columns, colWithSort) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + // Parse optional INCLUDE clause + if strings.ToUpper(p.curTok.Literal) == "INCLUDE" { + p.nextToken() // consume INCLUDE + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{p.parseIdentifier()}, + }, + } + colRef.MultiPartIdentifier.Count = len(colRef.MultiPartIdentifier.Identifiers) + indexDef.IncludeColumns = append(indexDef.IncludeColumns, colRef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } col.Index = indexDef } else if upperLit == "SPARSE" { p.nextToken() // consume SPARSE diff --git a/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json b/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json +++ b/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json b/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json +++ b/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 74b4588918fa0fc42b582fe1990bc4c81cbb097f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:28:10 +0000 Subject: [PATCH 16/29] Add BEGIN DIALOG and BEGIN CONVERSATION TIMER parsing Support Service Broker statements: - BEGIN DIALOG [CONVERSATION] with FROM/TO SERVICE clauses - WITH options: RELATED_CONVERSATION, RELATED_CONVERSATION_GROUP, ENCRYPTION, LIFETIME - BEGIN CONVERSATION TIMER with TIMEOUT --- ast/begin_dialog_statement.go | 45 ++++ parser/lexer.go | 2 + parser/marshal.go | 69 ++++++ parser/parse_statements.go | 219 ++++++++++++++++++ .../metadata.json | 2 +- .../metadata.json | 2 +- 6 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 ast/begin_dialog_statement.go diff --git a/ast/begin_dialog_statement.go b/ast/begin_dialog_statement.go new file mode 100644 index 00000000..04c572fa --- /dev/null +++ b/ast/begin_dialog_statement.go @@ -0,0 +1,45 @@ +package ast + +// BeginDialogStatement represents a BEGIN DIALOG statement for SQL Server Service Broker. +type BeginDialogStatement struct { + IsConversation bool `json:"IsConversation,omitempty"` + Handle ScalarExpression `json:"Handle,omitempty"` + InitiatorServiceName *IdentifierOrValueExpression `json:"InitiatorServiceName,omitempty"` + TargetServiceName ScalarExpression `json:"TargetServiceName,omitempty"` + ContractName *IdentifierOrValueExpression `json:"ContractName,omitempty"` + InstanceSpec ScalarExpression `json:"InstanceSpec,omitempty"` + Options []DialogOption `json:"Options,omitempty"` +} + +func (s *BeginDialogStatement) node() {} +func (s *BeginDialogStatement) statement() {} + +// BeginConversationTimerStatement represents a BEGIN CONVERSATION TIMER statement. +type BeginConversationTimerStatement struct { + Handle ScalarExpression `json:"Handle,omitempty"` + Timeout ScalarExpression `json:"Timeout,omitempty"` +} + +func (s *BeginConversationTimerStatement) node() {} +func (s *BeginConversationTimerStatement) statement() {} + +// DialogOption is an interface for dialog options. +type DialogOption interface { + dialogOption() +} + +// ScalarExpressionDialogOption represents a dialog option with a scalar expression value. +type ScalarExpressionDialogOption struct { + Value ScalarExpression `json:"Value,omitempty"` + OptionKind string `json:"OptionKind,omitempty"` // RelatedConversation, RelatedConversationGroup, Lifetime +} + +func (o *ScalarExpressionDialogOption) dialogOption() {} + +// OnOffDialogOption represents a dialog option with an ON/OFF value. +type OnOffDialogOption struct { + OptionState string `json:"OptionState,omitempty"` // On, Off + OptionKind string `json:"OptionKind,omitempty"` // Encryption +} + +func (o *OnOffDialogOption) dialogOption() {} diff --git a/parser/lexer.go b/parser/lexer.go index 9686f54d..51bf5a0a 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -185,6 +185,7 @@ const ( TokenColonColon TokenMove TokenConversation + TokenDialog TokenGet TokenUse TokenKill @@ -925,6 +926,7 @@ var keywords = map[string]TokenType{ "TRUNCATE": TokenTruncate, "MOVE": TokenMove, "CONVERSATION": TokenConversation, + "DIALOG": TokenDialog, "GET": TokenGet, "USE": TokenUse, "KILL": TokenKill, diff --git a/parser/marshal.go b/parser/marshal.go index 222df3aa..86ab76f5 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -72,6 +72,10 @@ func statementToJSON(stmt ast.Statement) jsonNode { return beginEndBlockStatementToJSON(s) case *ast.BeginEndAtomicBlockStatement: return beginEndAtomicBlockStatementToJSON(s) + case *ast.BeginDialogStatement: + return beginDialogStatementToJSON(s) + case *ast.BeginConversationTimerStatement: + return beginConversationTimerStatementToJSON(s) case *ast.CreateViewStatement: return createViewStatementToJSON(s) case *ast.CreateSchemaStatement: @@ -2861,6 +2865,71 @@ func statementListToJSON(sl *ast.StatementList) jsonNode { return node } +func beginDialogStatementToJSON(s *ast.BeginDialogStatement) jsonNode { + node := jsonNode{ + "$type": "BeginDialogStatement", + "IsConversation": s.IsConversation, + } + if s.Handle != nil { + node["Handle"] = scalarExpressionToJSON(s.Handle) + } + if s.InitiatorServiceName != nil { + node["InitiatorServiceName"] = identifierOrValueExpressionToJSON(s.InitiatorServiceName) + } + if s.TargetServiceName != nil { + node["TargetServiceName"] = scalarExpressionToJSON(s.TargetServiceName) + } + if s.ContractName != nil { + node["ContractName"] = identifierOrValueExpressionToJSON(s.ContractName) + } + if s.InstanceSpec != nil { + node["InstanceSpec"] = scalarExpressionToJSON(s.InstanceSpec) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = dialogOptionToJSON(o) + } + node["Options"] = options + } + return node +} + +func dialogOptionToJSON(o ast.DialogOption) jsonNode { + switch opt := o.(type) { + case *ast.ScalarExpressionDialogOption: + node := jsonNode{ + "$type": "ScalarExpressionDialogOption", + "OptionKind": opt.OptionKind, + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + return node + case *ast.OnOffDialogOption: + return jsonNode{ + "$type": "OnOffDialogOption", + "OptionState": opt.OptionState, + "OptionKind": opt.OptionKind, + } + default: + return jsonNode{"$type": "UnknownDialogOption"} + } +} + +func beginConversationTimerStatementToJSON(s *ast.BeginConversationTimerStatement) jsonNode { + node := jsonNode{ + "$type": "BeginConversationTimerStatement", + } + if s.Handle != nil { + node["Handle"] = scalarExpressionToJSON(s.Handle) + } + if s.Timeout != nil { + node["Timeout"] = scalarExpressionToJSON(s.Timeout) + } + return node +} + func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { node := jsonNode{ "$type": "CreateViewStatement", diff --git a/parser/parse_statements.go b/parser/parse_statements.go index cd8179c4..e54e4392 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -1318,6 +1318,10 @@ func (p *Parser) parseBeginStatement() (ast.Statement, error) { return p.parseBeginTransactionStatementContinued(false) case TokenTry: return p.parseTryCatchStatement() + case TokenDialog: + return p.parseBeginDialogStatement() + case TokenConversation: + return p.parseBeginConversationTimerStatement() case TokenIdent: // Check for DISTRIBUTED if strings.ToUpper(p.curTok.Literal) == "DISTRIBUTED" { @@ -1660,6 +1664,221 @@ func (p *Parser) parseBeginEndBlockStatementContinued() (*ast.BeginEndBlockState return stmt, nil } +func (p *Parser) parseBeginDialogStatement() (*ast.BeginDialogStatement, error) { + p.nextToken() // consume DIALOG + + stmt := &ast.BeginDialogStatement{} + + // Check for optional CONVERSATION keyword + if p.curTok.Type == TokenConversation { + stmt.IsConversation = true + p.nextToken() // consume CONVERSATION + } + + // Parse dialog handle (variable reference) + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected variable for dialog handle") + } + + // Parse FROM SERVICE + if p.curTok.Type != TokenFrom { + return nil, fmt.Errorf("expected FROM after dialog handle") + } + p.nextToken() // consume FROM + + if strings.ToUpper(p.curTok.Literal) != "SERVICE" { + return nil, fmt.Errorf("expected SERVICE after FROM") + } + p.nextToken() // consume SERVICE + + // Parse initiator service name (identifier) + id := p.parseIdentifier() + stmt.InitiatorServiceName = &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + } + + // Parse TO SERVICE + if p.curTok.Type != TokenTo { + return nil, fmt.Errorf("expected TO after initiator service name") + } + p.nextToken() // consume TO + + if strings.ToUpper(p.curTok.Literal) != "SERVICE" { + return nil, fmt.Errorf("expected SERVICE after TO") + } + p.nextToken() // consume SERVICE + + // Parse target service name (string literal or variable) + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + stmt.TargetServiceName = strLit + } else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.TargetServiceName = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected string literal or variable for target service name") + } + + // Check for optional instance spec (after comma) + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + stmt.InstanceSpec = strLit + } else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.InstanceSpec = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } + } + + // Parse optional ON CONTRACT + if p.curTok.Type == TokenOn && strings.ToUpper(p.peekTok.Literal) == "CONTRACT" { + p.nextToken() // consume ON + p.nextToken() // consume CONTRACT + id := p.parseIdentifier() + stmt.ContractName = &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + } + } + + // Parse optional WITH options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + for { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + switch optName { + case "RELATED_CONVERSATION": + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.VariableReference{Name: p.curTok.Literal}, + OptionKind: "RelatedConversation", + }) + p.nextToken() + } + case "RELATED_CONVERSATION_GROUP": + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.VariableReference{Name: p.curTok.Literal}, + OptionKind: "RelatedConversationGroup", + }) + p.nextToken() + } + case "ENCRYPTION": + optState := strings.ToUpper(p.curTok.Literal) + if optState == "ON" { + stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{ + OptionState: "On", + OptionKind: "Encryption", + }) + } else if optState == "OFF" { + stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{ + OptionState: "Off", + OptionKind: "Encryption", + }) + } + p.nextToken() + case "LIFETIME": + if p.curTok.Type == TokenNumber { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + }, + OptionKind: "Lifetime", + }) + p.nextToken() + } + } + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseBeginConversationTimerStatement() (*ast.BeginConversationTimerStatement, error) { + p.nextToken() // consume CONVERSATION + + // Expect TIMER + if strings.ToUpper(p.curTok.Literal) != "TIMER" { + return nil, fmt.Errorf("expected TIMER after BEGIN CONVERSATION") + } + p.nextToken() // consume TIMER + + stmt := &ast.BeginConversationTimerStatement{} + + // Parse handle in parentheses + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after TIMER") + } + p.nextToken() // consume ( + + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected variable for conversation handle") + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after handle") + } + p.nextToken() // consume ) + + // Parse TIMEOUT = value + if strings.ToUpper(p.curTok.Literal) != "TIMEOUT" { + return nil, fmt.Errorf("expected TIMEOUT") + } + p.nextToken() // consume TIMEOUT + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + if p.curTok.Type == TokenNumber { + stmt.Timeout = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } else { + return nil, fmt.Errorf("expected integer for timeout value") + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, error) { // Consume BEGIN p.nextToken() diff --git a/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json b/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json +++ b/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BeginConversationStatementTests/metadata.json b/parser/testdata/BeginConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BeginConversationStatementTests/metadata.json +++ b/parser/testdata/BeginConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From aa6a1d0caf967aa8398022fb342e7b5fb2103c58 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:35:58 +0000 Subject: [PATCH 17/29] Fix ALTER INDEX option parsing issues - Add LOB_COMPACTION to option kind mapping for LobCompaction - Use IgnoreDupKeyIndexOption type for IGNORE_DUP_KEY option - Add missing LiteralType for IntegerLiteral in index options --- parser/marshal.go | 37 ++++++++++++++----- .../AlterIndexStatementTests/metadata.json | 2 +- .../metadata.json | 2 +- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/parser/marshal.go b/parser/marshal.go index 86ab76f5..3aca1151 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -7845,15 +7845,23 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { } stmt.IndexOptions = append(stmt.IndexOptions, opt) } else if valueUpper == "ON" || valueUpper == "OFF" { - opt := &ast.IndexStateOption{ - OptionKind: p.getIndexOptionKind(optionName), - OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), + if optionName == "IGNORE_DUP_KEY" { + opt := &ast.IgnoreDupKeyIndexOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else { + opt := &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) } - stmt.IndexOptions = append(stmt.IndexOptions, opt) } else { opt := &ast.IndexExpressionOption{ OptionKind: p.getIndexOptionKind(optionName), - Expression: &ast.IntegerLiteral{Value: valueStr}, + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, } stmt.IndexOptions = append(stmt.IndexOptions, opt) } @@ -7919,16 +7927,24 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { // Determine if it's a state option (ON/OFF) or expression option if valueStr == "ON" || valueStr == "OFF" { - opt := &ast.IndexStateOption{ - OptionKind: p.getIndexOptionKind(optionName), - OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + if optionName == "IGNORE_DUP_KEY" { + opt := &ast.IgnoreDupKeyIndexOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else { + opt := &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) } - stmt.IndexOptions = append(stmt.IndexOptions, opt) } else { // Expression option like FILLFACTOR = 80 opt := &ast.IndexExpressionOption{ OptionKind: p.getIndexOptionKind(optionName), - Expression: &ast.IntegerLiteral{Value: valueStr}, + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, } stmt.IndexOptions = append(stmt.IndexOptions, opt) } @@ -7972,6 +7988,7 @@ func (p *Parser) getIndexOptionKind(optionName string) string { "OPTIMIZE_FOR_SEQUENTIAL_KEY": "OptimizeForSequentialKey", "COMPRESS_ALL_ROW_GROUPS": "CompressAllRowGroups", "COMPRESSION_DELAY": "CompressionDelay", + "LOB_COMPACTION": "LobCompaction", } if kind, ok := optionMap[optionName]; ok { return kind diff --git a/parser/testdata/AlterIndexStatementTests/metadata.json b/parser/testdata/AlterIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterIndexStatementTests/metadata.json +++ b/parser/testdata/AlterIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json b/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json +++ b/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From d12e9224703f4f6328630a90223fb3623d1f2b2a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 09:41:23 +0000 Subject: [PATCH 18/29] Add comprehensive CREATE CERTIFICATE parsing Support source types (FROM ASSEMBLY, FROM FILE, FROM EXECUTABLE FILE), AUTHORIZATION, WITH PRIVATE KEY options (FILE, DECRYPTION BY PASSWORD, ENCRYPTION BY PASSWORD), certificate options (SUBJECT, START_DATE, EXPIRY_DATE), and ACTIVE FOR BEGIN_DIALOG. --- ast/create_simple_statements.go | 34 +++- parser/marshal.go | 46 +++++ parser/parse_statements.go | 189 +++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- 5 files changed, 267 insertions(+), 6 deletions(-) diff --git a/ast/create_simple_statements.go b/ast/create_simple_statements.go index ead90b2e..d091be8b 100644 --- a/ast/create_simple_statements.go +++ b/ast/create_simple_statements.go @@ -122,12 +122,44 @@ func (s *CreateAssemblyStatement) statement() {} // CreateCertificateStatement represents a CREATE CERTIFICATE statement. type CreateCertificateStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + Owner *Identifier `json:"Owner,omitempty"` + CertificateSource EncryptionSource `json:"CertificateSource,omitempty"` + ActiveForBeginDialog string `json:"ActiveForBeginDialog,omitempty"` // "On", "Off", "NotSet" + PrivateKeyPath *StringLiteral `json:"PrivateKeyPath,omitempty"` + EncryptionPassword *StringLiteral `json:"EncryptionPassword,omitempty"` + DecryptionPassword *StringLiteral `json:"DecryptionPassword,omitempty"` + CertificateOptions []*CertificateOption `json:"CertificateOptions,omitempty"` } func (s *CreateCertificateStatement) node() {} func (s *CreateCertificateStatement) statement() {} +// CertificateOption represents an option in a CREATE CERTIFICATE statement. +type CertificateOption struct { + Kind string `json:"Kind,omitempty"` // "Subject", "StartDate", "ExpiryDate" + Value *StringLiteral `json:"Value,omitempty"` +} + +func (o *CertificateOption) node() {} + +// AssemblyEncryptionSource represents a certificate source from an assembly. +type AssemblyEncryptionSource struct { + Assembly *Identifier `json:"Assembly,omitempty"` +} + +func (s *AssemblyEncryptionSource) node() {} +func (s *AssemblyEncryptionSource) encryptionSource() {} + +// FileEncryptionSource represents a certificate source from a file. +type FileEncryptionSource struct { + IsExecutable bool `json:"IsExecutable,omitempty"` + File *StringLiteral `json:"File,omitempty"` +} + +func (s *FileEncryptionSource) node() {} +func (s *FileEncryptionSource) encryptionSource() {} + // CreateAsymmetricKeyStatement represents a CREATE ASYMMETRIC KEY statement. type CreateAsymmetricKeyStatement struct { Name *Identifier `json:"Name,omitempty"` diff --git a/parser/marshal.go b/parser/marshal.go index 3aca1151..661e1e91 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -11876,6 +11876,23 @@ func encryptionSourceToJSON(source ast.EncryptionSource) interface{} { switch s := source.(type) { case *ast.ProviderEncryptionSource: return providerEncryptionSourceToJSON(s) + case *ast.AssemblyEncryptionSource: + node := jsonNode{ + "$type": "AssemblyEncryptionSource", + } + if s.Assembly != nil { + node["Assembly"] = identifierToJSON(s.Assembly) + } + return node + case *ast.FileEncryptionSource: + node := jsonNode{ + "$type": "FileEncryptionSource", + "IsExecutable": s.IsExecutable, + } + if s.File != nil { + node["File"] = stringLiteralToJSON(s.File) + } + return node default: return nil } @@ -11992,9 +12009,38 @@ func createCertificateStatementToJSON(s *ast.CreateCertificateStatement) jsonNod node := jsonNode{ "$type": "CreateCertificateStatement", } + if s.CertificateSource != nil { + node["CertificateSource"] = encryptionSourceToJSON(s.CertificateSource) + } + if len(s.CertificateOptions) > 0 { + options := make([]jsonNode, len(s.CertificateOptions)) + for i, opt := range s.CertificateOptions { + options[i] = jsonNode{ + "$type": "CertificateOption", + "Kind": opt.Kind, + "Value": stringLiteralToJSON(opt.Value), + } + } + node["CertificateOptions"] = options + } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.ActiveForBeginDialog != "" { + node["ActiveForBeginDialog"] = s.ActiveForBeginDialog + } + if s.PrivateKeyPath != nil { + node["PrivateKeyPath"] = stringLiteralToJSON(s.PrivateKeyPath) + } + if s.EncryptionPassword != nil { + node["EncryptionPassword"] = stringLiteralToJSON(s.EncryptionPassword) + } + if s.DecryptionPassword != nil { + node["DecryptionPassword"] = stringLiteralToJSON(s.DecryptionPassword) + } return node } diff --git a/parser/parse_statements.go b/parser/parse_statements.go index e54e4392..98bf207c 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -8676,11 +8676,194 @@ func (p *Parser) parseCreateCertificateStatement() (*ast.CreateCertificateStatem p.nextToken() // consume CERTIFICATE stmt := &ast.CreateCertificateStatement{ - Name: p.parseIdentifier(), + Name: p.parseIdentifier(), + ActiveForBeginDialog: "NotSet", + } + + // Optional AUTHORIZATION + if strings.ToUpper(p.curTok.Literal) == "AUTHORIZATION" { + p.nextToken() + stmt.Owner = p.parseIdentifier() + } + + // Optional ENCRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "ENCRYPTION" { + p.nextToken() // consume ENCRYPTION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.EncryptionPassword = strLit + } + } + } + + // Optional FROM clause + if p.curTok.Type == TokenFrom { + p.nextToken() // consume FROM + sourceType := strings.ToUpper(p.curTok.Literal) + + if sourceType == "ASSEMBLY" { + p.nextToken() // consume ASSEMBLY + stmt.CertificateSource = &ast.AssemblyEncryptionSource{ + Assembly: p.parseIdentifier(), + } + } else if sourceType == "FILE" || sourceType == "EXECUTABLE" { + isExecutable := false + if sourceType == "EXECUTABLE" { + isExecutable = true + p.nextToken() // consume EXECUTABLE + // Next should be FILE + } + if strings.ToUpper(p.curTok.Literal) == "FILE" { + p.nextToken() // consume FILE + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.CertificateSource = &ast.FileEncryptionSource{ + IsExecutable: isExecutable, + File: strLit, + } + } + } + } + } + + // Parse WITH clauses (can appear multiple times for different purposes) + for p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + + // Check if it's PRIVATE KEY or certificate options + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "PRIVATE" { + p.nextToken() // consume PRIVATE + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + switch optName { + case "FILE": + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.PrivateKeyPath = strLit + } + case "DECRYPTION": + // DECRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.DecryptionPassword = strLit + } + } + case "ENCRYPTION": + // ENCRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.EncryptionPassword = strLit + } + } + } + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + // Certificate options: SUBJECT, START_DATE, EXPIRY_DATE + for { + optName := strings.ToUpper(p.curTok.Literal) + if optName != "SUBJECT" && optName != "START_DATE" && optName != "EXPIRY_DATE" { + break + } + p.nextToken() // consume option name + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + kind := "" + switch optName { + case "SUBJECT": + kind = "Subject" + case "START_DATE": + kind = "StartDate" + case "EXPIRY_DATE": + kind = "ExpiryDate" + } + stmt.CertificateOptions = append(stmt.CertificateOptions, &ast.CertificateOption{ + Kind: kind, + Value: strLit, + }) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + } + + // Optional ACTIVE FOR BEGIN_DIALOG + if strings.ToUpper(p.curTok.Literal) == "ACTIVE" { + p.nextToken() // consume ACTIVE + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + } + if strings.ToUpper(p.curTok.Literal) == "BEGIN_DIALOG" { + p.nextToken() // consume BEGIN_DIALOG + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "ON" { + stmt.ActiveForBeginDialog = "On" + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "OFF" { + stmt.ActiveForBeginDialog = "Off" + p.nextToken() + } + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() } - // Skip rest of statement - p.skipToEndOfStatement() return stmt, nil } diff --git a/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json b/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json +++ b/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateCertificateStatementTests/metadata.json b/parser/testdata/CreateCertificateStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateCertificateStatementTests/metadata.json +++ b/parser/testdata/CreateCertificateStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From a3dbf524e8464a7ee02b868b9e0394f4f830f52e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:05:47 +0000 Subject: [PATCH 19/29] Add IDENTITY function call and IDENTITYCOL/ROWGUIDCOL support - Add IdentityFunctionCall AST type for IDENTITY(data_type [, seed, increment]) - Add parsing for IDENTITY function calls - Add handling for IDENTITYCOL and ROWGUIDCOL column types in expressions - Handle multi-part identifiers with empty parts (e.g., master..t1.IDENTITYCOL) - Fix national string handling in column aliases (AS N'alias') - Add JSON marshaling for IdentityFunctionCall This enables the BaselinesCommon_SelectExpressionTests test. --- ast/function_call.go | 10 + parser/marshal.go | 14 ++ parser/parse_select.go | 227 +++++++++++++++++- .../metadata.json | 2 +- 4 files changed, 243 insertions(+), 10 deletions(-) diff --git a/ast/function_call.go b/ast/function_call.go index 67b113e1..ff3cd9fc 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -97,3 +97,13 @@ type TryConvertCall struct { func (*TryConvertCall) node() {} func (*TryConvertCall) scalarExpression() {} + +// IdentityFunctionCall represents an IDENTITY function call: IDENTITY(data_type [, seed, increment]) +type IdentityFunctionCall struct { + DataType DataTypeReference `json:"DataType,omitempty"` + Seed ScalarExpression `json:"Seed,omitempty"` + Increment ScalarExpression `json:"Increment,omitempty"` +} + +func (*IdentityFunctionCall) node() {} +func (*IdentityFunctionCall) scalarExpression() {} diff --git a/parser/marshal.go b/parser/marshal.go index 661e1e91..c09710cc 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -1488,6 +1488,20 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["Collation"] = identifierToJSON(e.Collation) } return node + case *ast.IdentityFunctionCall: + node := jsonNode{ + "$type": "IdentityFunctionCall", + } + if e.DataType != nil { + node["DataType"] = dataTypeReferenceToJSON(e.DataType) + } + if e.Seed != nil { + node["Seed"] = scalarExpressionToJSON(e.Seed) + } + if e.Increment != nil { + node["Increment"] = scalarExpressionToJSON(e.Increment) + } + return node case *ast.BinaryExpression: node := jsonNode{ "$type": "BinaryExpression", diff --git a/parser/parse_select.go b/parser/parse_select.go index 2628d8c6..b0c92af4 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -418,10 +418,42 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } // Not an assignment, treat as regular scalar expression starting with variable - // We need to "un-consume" the variable and let parseScalarExpression handle it - // Create the variable reference and use it as the expression varRef := &ast.VariableReference{Name: varName} - sse := &ast.SelectScalarExpression{Expression: varRef} + + // Check if next token is a binary operator - if so, continue parsing the expression + var expr ast.ScalarExpression = varRef + for p.curTok.Type == TokenPlus || p.curTok.Type == TokenMinus || + p.curTok.Type == TokenStar || p.curTok.Type == TokenSlash || + p.curTok.Type == TokenPercent || p.curTok.Type == TokenDoublePipe { + // We have a variable followed by a binary operator, continue parsing + var opType string + switch p.curTok.Type { + case TokenPlus: + opType = "Add" + case TokenMinus: + opType = "Subtract" + case TokenStar: + opType = "Multiply" + case TokenSlash: + opType = "Divide" + case TokenPercent: + opType = "Modulo" + case TokenDoublePipe: + opType = "Add" // String concatenation + } + p.nextToken() // consume operator + right, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + expr = &ast.BinaryExpression{ + FirstExpression: expr, + SecondExpression: right, + BinaryExpressionType: opType, + } + } + + sse := &ast.SelectScalarExpression{Expression: expr} // Check for column alias if p.curTok.Type == TokenIdent && p.curTok.Literal[0] == '[' { @@ -512,6 +544,13 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { Value: str.Value, ValueExpression: str, } + } else if p.curTok.Type == TokenNationalString { + // National string literal alias: AS N'alias' + str, _ := p.parseNationalStringFromToken() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: str.Value, + ValueExpression: str, + } } else { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ @@ -735,6 +774,17 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) { if upper == "TRY_CONVERT" && p.peekTok.Type == TokenLParen { return p.parseTryConvertCall() } + if upper == "IDENTITY" && p.peekTok.Type == TokenLParen { + return p.parseIdentityFunctionCall() + } + if upper == "IDENTITYCOL" { + p.nextToken() + return &ast.ColumnReferenceExpression{ColumnType: "IdentityCol"}, nil + } + if upper == "ROWGUIDCOL" { + p.nextToken() + return &ast.ColumnReferenceExpression{ColumnType: "RowGuidCol"}, nil + } return p.parseColumnReferenceOrFunctionCall() case TokenNumber: val := p.curTok.Literal @@ -785,6 +835,13 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) { // Wildcard column reference (e.g., * in count(*)) p.nextToken() return &ast.ColumnReferenceExpression{ColumnType: "Wildcard"}, nil + case TokenDot: + // Multi-part identifier starting with empty parts (e.g., ..t1.c1) + return p.parseColumnReferenceWithLeadingDots() + case TokenMaster, TokenDatabase, TokenKey, TokenTable, TokenIndex, + TokenSchema, TokenUser, TokenView: + // Keywords that can be used as identifiers in column/table references + return p.parseColumnReferenceOrFunctionCall() default: return nil, fmt.Errorf("unexpected token in expression: %s", p.curTok.Literal) } @@ -1059,20 +1116,43 @@ func (p *Parser) parseNationalStringFromToken() (*ast.StringLiteral, error) { }, nil } +func (p *Parser) isIdentifierToken() bool { + switch p.curTok.Type { + case TokenIdent, TokenMaster, TokenDatabase, TokenKey, TokenTable, TokenIndex, + TokenSchema, TokenUser, TokenView, TokenDefault: + return true + default: + return false + } +} + func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, error) { var identifiers []*ast.Identifier + colType := "Regular" for { - if p.curTok.Type != TokenIdent { + if !p.isIdentifierToken() { break } quoteType := "NotQuoted" literal := p.curTok.Literal + upper := strings.ToUpper(literal) + // Handle bracketed identifiers if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { quoteType = "SquareBracket" literal = literal[1 : len(literal)-1] + } else if upper == "IDENTITYCOL" || upper == "ROWGUIDCOL" { + // IDENTITYCOL/ROWGUIDCOL at end of multi-part identifier sets column type + // and is not included in the identifier list + if upper == "IDENTITYCOL" { + colType = "IdentityCol" + } else { + colType = "RowGuidCol" + } + p.nextToken() + break } id := &ast.Identifier{ @@ -1091,6 +1171,12 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err break } p.nextToken() // consume dot + + // Handle consecutive dots (empty parts in multi-part identifier) + for p.curTok.Type == TokenDot { + identifiers = append(identifiers, &ast.Identifier{Value: "", QuoteType: "NotQuoted"}) + p.nextToken() // consume dot + } } // Check for :: (user-defined type method call or property access): a.b::func() or a::prop @@ -1169,12 +1255,21 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err return p.parseFunctionCallFromIdentifiers(identifiers) } + // If we have identifiers, build a column reference with them + if len(identifiers) > 0 { + return &ast.ColumnReferenceExpression{ + ColumnType: colType, + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil + } + + // No identifiers means just IDENTITYCOL or ROWGUIDCOL (already handled in parsePrimaryExpression) + // but handle the case anyway return &ast.ColumnReferenceExpression{ - ColumnType: "Regular", - MultiPartIdentifier: &ast.MultiPartIdentifier{ - Count: len(identifiers), - Identifiers: identifiers, - }, + ColumnType: colType, }, nil } @@ -1190,6 +1285,71 @@ func (p *Parser) parseColumnReference() (*ast.ColumnReferenceExpression, error) return nil, fmt.Errorf("expected column reference, got function call") } +func (p *Parser) parseColumnReferenceWithLeadingDots() (ast.ScalarExpression, error) { + // Handle multi-part identifiers starting with dots like ..t1.c1 or .db..t1.c1 + var identifiers []*ast.Identifier + + // Add empty identifiers for leading dots + for p.curTok.Type == TokenDot { + identifiers = append(identifiers, &ast.Identifier{Value: "", QuoteType: "NotQuoted"}) + p.nextToken() // consume dot + } + + // Now parse the remaining identifiers + for p.isIdentifierToken() { + quoteType := "NotQuoted" + literal := p.curTok.Literal + // Handle special column types + upper := strings.ToUpper(literal) + if upper == "IDENTITYCOL" || upper == "ROWGUIDCOL" { + // Return with the proper column type + colType := "IdentityCol" + if upper == "ROWGUIDCOL" { + colType = "RowGuidCol" + } + p.nextToken() + return &ast.ColumnReferenceExpression{ + ColumnType: colType, + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil + } + // Handle bracketed identifiers + if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { + quoteType = "SquareBracket" + literal = literal[1 : len(literal)-1] + } + + id := &ast.Identifier{ + Value: literal, + QuoteType: quoteType, + } + identifiers = append(identifiers, id) + p.nextToken() + + if p.curTok.Type != TokenDot { + break + } + // Check for qualified star + if p.peekTok.Type == TokenStar { + break + } + p.nextToken() // consume dot + } + + // Don't consume .* here - let the caller (parseSelectElement) handle qualified stars + + return &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil +} + func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (ast.ScalarExpression, error) { fc := &ast.FunctionCall{ UniqueRowFilter: "NotSpecified", @@ -3098,6 +3258,55 @@ func (p *Parser) parseTryConvertCall() (ast.ScalarExpression, error) { return convert, nil } +// parseIdentityFunctionCall parses an IDENTITY function call: IDENTITY(data_type [, seed, increment]) +func (p *Parser) parseIdentityFunctionCall() (ast.ScalarExpression, error) { + p.nextToken() // consume IDENTITY + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse the data type + dt, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + + identity := &ast.IdentityFunctionCall{ + DataType: dt, + } + + // Check for optional seed and increment + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + seed, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + identity.Seed = seed + + // Expect comma before increment + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , before increment in IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + increment, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + identity.Increment = increment + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + return identity, nil +} + // parsePredictTableReference parses PREDICT(...) in FROM clause // PREDICT(MODEL = expression, DATA = table AS alias, RUNTIME=ident) WITH (columns) AS alias func (p *Parser) parsePredictTableReference() (*ast.PredictTableReference, error) { diff --git a/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json b/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json +++ b/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From c48d32d39d0808ea2d53b80b7b8b5dd1b7cd9a70 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:13:47 +0000 Subject: [PATCH 20/29] Add CREATE OR ALTER support for procedure, trigger, and view statements - Add CreateOrAlterProcedureStatement AST type - Add CreateOrAlterTriggerStatement AST type - Add CreateOrAlterViewStatement AST type - Update parsing to return correct CreateOrAlter types - Add JSON marshaling for all three new types This enables 2 tests: Baselines130_CreateOrAlterStatementTests130 and CreateOrAlterStatementTests130. --- ast/create_procedure_statement.go | 13 ++ ast/create_trigger_statement.go | 16 ++ ast/create_view_statement.go | 13 ++ parser/marshal.go | 153 ++++++++++++++++-- .../metadata.json | 2 +- .../metadata.json | 2 +- 6 files changed, 188 insertions(+), 11 deletions(-) diff --git a/ast/create_procedure_statement.go b/ast/create_procedure_statement.go index ed020c4d..919c86b0 100644 --- a/ast/create_procedure_statement.go +++ b/ast/create_procedure_statement.go @@ -13,6 +13,19 @@ type CreateProcedureStatement struct { func (c *CreateProcedureStatement) node() {} func (c *CreateProcedureStatement) statement() {} +// CreateOrAlterProcedureStatement represents a CREATE OR ALTER PROCEDURE statement. +type CreateOrAlterProcedureStatement struct { + ProcedureReference *ProcedureReference + Parameters []*ProcedureParameter + StatementList *StatementList + IsForReplication bool + Options []ProcedureOptionBase + MethodSpecifier *MethodSpecifier +} + +func (c *CreateOrAlterProcedureStatement) node() {} +func (c *CreateOrAlterProcedureStatement) statement() {} + // ProcedureParameter represents a parameter in a procedure definition. type ProcedureParameter struct { VariableName *Identifier diff --git a/ast/create_trigger_statement.go b/ast/create_trigger_statement.go index 00543360..60ede459 100644 --- a/ast/create_trigger_statement.go +++ b/ast/create_trigger_statement.go @@ -16,6 +16,22 @@ type CreateTriggerStatement struct { func (s *CreateTriggerStatement) statement() {} func (s *CreateTriggerStatement) node() {} +// CreateOrAlterTriggerStatement represents a CREATE OR ALTER TRIGGER statement +type CreateOrAlterTriggerStatement struct { + Name *SchemaObjectName + TriggerObject *TriggerObject + TriggerType string // "For", "After", "InsteadOf" + TriggerActions []*TriggerAction + Options []TriggerOptionType + WithAppend bool + IsNotForReplication bool + MethodSpecifier *MethodSpecifier + StatementList *StatementList +} + +func (s *CreateOrAlterTriggerStatement) statement() {} +func (s *CreateOrAlterTriggerStatement) node() {} + // EventTypeContainer represents an event type container type EventTypeContainer struct { EventType string `json:"EventType,omitempty"` diff --git a/ast/create_view_statement.go b/ast/create_view_statement.go index 44e69ae7..480c7a2a 100644 --- a/ast/create_view_statement.go +++ b/ast/create_view_statement.go @@ -13,6 +13,19 @@ type CreateViewStatement struct { func (c *CreateViewStatement) node() {} func (c *CreateViewStatement) statement() {} +// CreateOrAlterViewStatement represents a CREATE OR ALTER VIEW statement. +type CreateOrAlterViewStatement struct { + SchemaObjectName *SchemaObjectName `json:"SchemaObjectName,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` + SelectStatement *SelectStatement `json:"SelectStatement,omitempty"` + WithCheckOption bool `json:"WithCheckOption"` + ViewOptions []ViewOption `json:"ViewOptions,omitempty"` + IsMaterialized bool `json:"IsMaterialized"` +} + +func (c *CreateOrAlterViewStatement) node() {} +func (c *CreateOrAlterViewStatement) statement() {} + // ViewOption is an interface for different view option types. type ViewOption interface { viewOption() diff --git a/parser/marshal.go b/parser/marshal.go index c09710cc..112fd63e 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -78,10 +78,14 @@ func statementToJSON(stmt ast.Statement) jsonNode { return beginConversationTimerStatementToJSON(s) case *ast.CreateViewStatement: return createViewStatementToJSON(s) + case *ast.CreateOrAlterViewStatement: + return createOrAlterViewStatementToJSON(s) case *ast.CreateSchemaStatement: return createSchemaStatementToJSON(s) case *ast.CreateProcedureStatement: return createProcedureStatementToJSON(s) + case *ast.CreateOrAlterProcedureStatement: + return createOrAlterProcedureStatementToJSON(s) case *ast.AlterProcedureStatement: return alterProcedureStatementToJSON(s) case *ast.CreateRoleStatement: @@ -380,6 +384,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterTriggerStatementToJSON(s) case *ast.CreateTriggerStatement: return createTriggerStatementToJSON(s) + case *ast.CreateOrAlterTriggerStatement: + return createOrAlterTriggerStatementToJSON(s) case *ast.EnableDisableTriggerStatement: return enableDisableTriggerStatementToJSON(s) case *ast.EndConversationStatement: @@ -2973,6 +2979,35 @@ func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { return node } +func createOrAlterViewStatementToJSON(s *ast.CreateOrAlterViewStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterViewStatement", + } + if s.SchemaObjectName != nil { + node["SchemaObjectName"] = schemaObjectNameToJSON(s.SchemaObjectName) + } + if len(s.Columns) > 0 { + cols := make([]jsonNode, len(s.Columns)) + for i, c := range s.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if len(s.ViewOptions) > 0 { + opts := make([]jsonNode, len(s.ViewOptions)) + for i, opt := range s.ViewOptions { + opts[i] = viewOptionToJSON(opt) + } + node["ViewOptions"] = opts + } + if s.SelectStatement != nil { + node["SelectStatement"] = selectStatementToJSON(s.SelectStatement) + } + node["WithCheckOption"] = s.WithCheckOption + node["IsMaterialized"] = s.IsMaterialized + return node +} + func viewOptionToJSON(opt ast.ViewOption) jsonNode { switch o := opt.(type) { case *ast.ViewStatementOption: @@ -6339,6 +6374,37 @@ func createProcedureStatementToJSON(s *ast.CreateProcedureStatement) jsonNode { return node } +func createOrAlterProcedureStatementToJSON(s *ast.CreateOrAlterProcedureStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterProcedureStatement", + "IsForReplication": s.IsForReplication, + } + if s.ProcedureReference != nil { + node["ProcedureReference"] = procedureReferenceToJSON(s.ProcedureReference) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + options[i] = procedureOptionToJSON(opt) + } + node["Options"] = options + } + if len(s.Parameters) > 0 { + params := make([]jsonNode, len(s.Parameters)) + for i, p := range s.Parameters { + params[i] = procedureParameterToJSON(p) + } + node["Parameters"] = params + } + if s.MethodSpecifier != nil { + node["MethodSpecifier"] = methodSpecifierToJSON(s.MethodSpecifier) + } + if s.StatementList != nil { + node["StatementList"] = statementListToJSON(s.StatementList) + } + return node +} + func procedureOptionToJSON(opt ast.ProcedureOptionBase) jsonNode { switch o := opt.(type) { case *ast.ProcedureOption: @@ -8553,21 +8619,57 @@ func (p *Parser) parseCreateOrAlterFunctionStatement() (*ast.CreateOrAlterFuncti } // parseCreateOrAlterProcedureStatement parses a CREATE OR ALTER PROCEDURE statement -func (p *Parser) parseCreateOrAlterProcedureStatement() (*ast.CreateProcedureStatement, error) { - // For now, delegate to regular CREATE PROCEDURE parsing - return p.parseCreateProcedureStatement() +func (p *Parser) parseCreateOrAlterProcedureStatement() (*ast.CreateOrAlterProcedureStatement, error) { + // Parse as regular CREATE PROCEDURE, then convert to CreateOrAlter type + stmt, err := p.parseCreateProcedureStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterProcedureStatement{ + ProcedureReference: stmt.ProcedureReference, + Parameters: stmt.Parameters, + StatementList: stmt.StatementList, + IsForReplication: stmt.IsForReplication, + Options: stmt.Options, + MethodSpecifier: stmt.MethodSpecifier, + }, nil } // parseCreateOrAlterViewStatement parses a CREATE OR ALTER VIEW statement -func (p *Parser) parseCreateOrAlterViewStatement() (*ast.CreateViewStatement, error) { - // For now, delegate to regular CREATE VIEW parsing - return p.parseCreateViewStatement() +func (p *Parser) parseCreateOrAlterViewStatement() (*ast.CreateOrAlterViewStatement, error) { + // Parse as regular CREATE VIEW, then convert to CreateOrAlter type + stmt, err := p.parseCreateViewStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterViewStatement{ + SchemaObjectName: stmt.SchemaObjectName, + Columns: stmt.Columns, + SelectStatement: stmt.SelectStatement, + WithCheckOption: stmt.WithCheckOption, + ViewOptions: stmt.ViewOptions, + IsMaterialized: stmt.IsMaterialized, + }, nil } // parseCreateOrAlterTriggerStatement parses a CREATE OR ALTER TRIGGER statement -func (p *Parser) parseCreateOrAlterTriggerStatement() (*ast.CreateTriggerStatement, error) { - // For now, delegate to regular CREATE TRIGGER parsing - return p.parseCreateTriggerStatement() +func (p *Parser) parseCreateOrAlterTriggerStatement() (*ast.CreateOrAlterTriggerStatement, error) { + // Parse as regular CREATE TRIGGER, then convert to CreateOrAlter type + stmt, err := p.parseCreateTriggerStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterTriggerStatement{ + Name: stmt.Name, + TriggerObject: stmt.TriggerObject, + TriggerType: stmt.TriggerType, + TriggerActions: stmt.TriggerActions, + Options: stmt.Options, + WithAppend: stmt.WithAppend, + IsNotForReplication: stmt.IsNotForReplication, + MethodSpecifier: stmt.MethodSpecifier, + StatementList: stmt.StatementList, + }, nil } // parseCreateTriggerStatement parses a CREATE TRIGGER statement @@ -9580,6 +9682,39 @@ func createTriggerStatementToJSON(s *ast.CreateTriggerStatement) jsonNode { return node } +func createOrAlterTriggerStatementToJSON(s *ast.CreateOrAlterTriggerStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterTriggerStatement", + "TriggerType": s.TriggerType, + "WithAppend": s.WithAppend, + "IsNotForReplication": s.IsNotForReplication, + } + if s.Name != nil { + node["Name"] = schemaObjectNameToJSON(s.Name) + } + if s.TriggerObject != nil { + node["TriggerObject"] = triggerObjectToJSON(s.TriggerObject) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = triggerOptionTypeToJSON(o) + } + node["Options"] = options + } + if len(s.TriggerActions) > 0 { + actions := make([]jsonNode, len(s.TriggerActions)) + for i, a := range s.TriggerActions { + actions[i] = triggerActionToJSON(a) + } + node["TriggerActions"] = actions + } + if s.StatementList != nil { + node["StatementList"] = statementListToJSON(s.StatementList) + } + return node +} + func triggerOptionTypeToJSON(o ast.TriggerOptionType) jsonNode { switch opt := o.(type) { case *ast.TriggerOption: diff --git a/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json b/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json +++ b/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateOrAlterStatementTests130/metadata.json b/parser/testdata/CreateOrAlterStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateOrAlterStatementTests130/metadata.json +++ b/parser/testdata/CreateOrAlterStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 7bcebdf120255cc357721341244aa45496dffb61 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:19:28 +0000 Subject: [PATCH 21/29] Add SQL 80-style CREATE INDEX WITH clause parsing - Add parseCreateIndexOptions80Style function for old-style syntax without parentheses - Handle WITH FILLFACTOR = 23, PAD_INDEX style options - Set Translated80SyntaxTo90 = true for old-style syntax - Use IndexStateOption for IGNORE_DUP_KEY in SQL 80 style This enables 2 tests: Baselines80_CreateIndexStatementTests and CreateIndexStatementTests. --- parser/parse_statements.go | 107 +++++++++++++++++- .../metadata.json | 2 +- .../CreateIndexStatementTests/metadata.json | 2 +- 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 98bf207c..6fb13cb1 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -7936,7 +7936,14 @@ func (p *Parser) parseCreateIndexStatement() (*ast.CreateIndexStatement, error) // Parse WITH (index options) if p.curTok.Type == TokenWith { p.nextToken() // consume WITH - stmt.IndexOptions = p.parseCreateIndexOptions() + // Check if this is SQL 80 style (no parentheses) or modern style (with parentheses) + if p.curTok.Type == TokenLParen { + stmt.IndexOptions = p.parseCreateIndexOptions() + } else { + // SQL 80 style - no parentheses around options + stmt.Translated80SyntaxTo90 = true + stmt.IndexOptions = p.parseCreateIndexOptions80Style() + } } // Parse ON filegroup/partition_scheme @@ -8050,6 +8057,104 @@ func (p *Parser) parseCreateIndexOptions() []ast.IndexOption { return options } +// parseCreateIndexOptions80Style parses index options without parentheses (SQL 80 style) +// e.g., WITH FILLFACTOR = 23, PAD_INDEX +func (p *Parser) parseCreateIndexOptions80Style() []ast.IndexOption { + var options []ast.IndexOption + + for { + // Check if current token could be an index option + upper := strings.ToUpper(p.curTok.Literal) + if !p.isIndexOption80Style(upper) { + break + } + + optionName := upper + p.nextToken() // consume option name + + var valueStr string + var valueToken Token + + // Check if there's an = value + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + valueToken = p.curTok + valueStr = strings.ToUpper(valueToken.Literal) + p.nextToken() // consume value + } else { + // No value means this is a flag option that is ON + valueStr = "ON" + } + + switch optionName { + case "PAD_INDEX": + options = append(options, &ast.IndexStateOption{ + OptionKind: "PadIndex", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "FILLFACTOR": + options = append(options, &ast.IndexExpressionOption{ + OptionKind: "FillFactor", + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueToken.Literal}, + }) + case "IGNORE_DUP_KEY": + // In SQL 80 style, IGNORE_DUP_KEY uses IndexStateOption + options = append(options, &ast.IndexStateOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "DROP_EXISTING": + options = append(options, &ast.IndexStateOption{ + OptionKind: "DropExisting", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "STATISTICS_NORECOMPUTE": + options = append(options, &ast.IndexStateOption{ + OptionKind: "StatisticsNoRecompute", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "SORT_IN_TEMPDB": + options = append(options, &ast.IndexStateOption{ + OptionKind: "SortInTempDB", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + default: + // Generic handling for other options + if valueStr == "ON" || valueStr == "OFF" { + options = append(options, &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + } else if valueToken.Type == TokenNumber || valueToken.Type != 0 { + options = append(options, &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueToken.Literal}, + }) + } + } + + // Check for comma to continue parsing options + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + return options +} + +// isIndexOption80Style checks if a token could be an index option in SQL 80 style +func (p *Parser) isIndexOption80Style(name string) bool { + switch name { + case "PAD_INDEX", "FILLFACTOR", "IGNORE_DUP_KEY", "DROP_EXISTING", + "STATISTICS_NORECOMPUTE", "SORT_IN_TEMPDB": + return true + default: + return false + } +} + func (p *Parser) parseCreateSpatialIndexStatement() (*ast.CreateSpatialIndexStatement, error) { p.nextToken() // consume SPATIAL if p.curTok.Type == TokenIndex { diff --git a/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json b/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json +++ b/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateIndexStatementTests/metadata.json b/parser/testdata/CreateIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests/metadata.json +++ b/parser/testdata/CreateIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 021f9743adde7d23bfd2e21b0696ccd3f1eb8e04 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:33:29 +0000 Subject: [PATCH 22/29] Add comprehensive EXECUTE statement parsing support - Add named parameters (@name = value) support - Add DEFAULT keyword as parameter value - Add OUTPUT modifier on parameters - Add procedure numbers (;1 suffix) parsing - Add OPENDATASOURCE/OPENROWSET ad-hoc data source support - Add IdentifierLiteral for bare identifier parameters - Fix DefaultLiteral to include LiteralType field - Add AdHocDataSource AST type and JSON marshaling Enables BaselinesCommon_ExecuteStatementTests --- ast/execute_statement.go | 14 +- parser/marshal.go | 19 +++ parser/parse_dml.go | 141 +++++++++++++++++- .../metadata.json | 2 +- 4 files changed, 166 insertions(+), 10 deletions(-) diff --git a/ast/execute_statement.go b/ast/execute_statement.go index bcfb8c5e..3f90e878 100644 --- a/ast/execute_statement.go +++ b/ast/execute_statement.go @@ -81,6 +81,7 @@ type ExecutableEntity interface { type ExecutableProcedureReference struct { ProcedureReference *ProcedureReferenceName `json:"ProcedureReference,omitempty"` Parameters []*ExecuteParameter `json:"Parameters,omitempty"` + AdHocDataSource *AdHocDataSource `json:"AdHocDataSource,omitempty"` } func (e *ExecutableProcedureReference) executableEntity() {} @@ -102,12 +103,19 @@ type ProcedureReferenceName struct { // ProcedureReference references a stored procedure by name. type ProcedureReference struct { - Name *SchemaObjectName `json:"Name,omitempty"` + Name *SchemaObjectName `json:"Name,omitempty"` + Number *IntegerLiteral `json:"Number,omitempty"` } // ExecuteParameter represents a parameter to an EXEC call. type ExecuteParameter struct { - ParameterValue ScalarExpression `json:"ParameterValue,omitempty"` + ParameterValue ScalarExpression `json:"ParameterValue,omitempty"` Variable *VariableReference `json:"Variable,omitempty"` - IsOutput bool `json:"IsOutput"` + IsOutput bool `json:"IsOutput"` +} + +// AdHocDataSource represents an OPENDATASOURCE or OPENROWSET call for ad-hoc data access. +type AdHocDataSource struct { + ProviderName *StringLiteral `json:"ProviderName,omitempty"` + InitString *StringLiteral `json:"InitString,omitempty"` } diff --git a/parser/marshal.go b/parser/marshal.go index 112fd63e..d3ce6adf 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -2406,6 +2406,9 @@ func executableEntityToJSON(entity ast.ExecutableEntity) jsonNode { } node["Parameters"] = params } + if e.AdHocDataSource != nil { + node["AdHocDataSource"] = adHocDataSourceToJSON(e.AdHocDataSource) + } return node case *ast.ExecutableStringList: node := jsonNode{ @@ -2451,6 +2454,9 @@ func procedureReferenceToJSON(pr *ast.ProcedureReference) jsonNode { if pr.Name != nil { node["Name"] = schemaObjectNameToJSON(pr.Name) } + if pr.Number != nil { + node["Number"] = scalarExpressionToJSON(pr.Number) + } return node } @@ -2468,6 +2474,19 @@ func executeParameterToJSON(ep *ast.ExecuteParameter) jsonNode { return node } +func adHocDataSourceToJSON(ds *ast.AdHocDataSource) jsonNode { + node := jsonNode{ + "$type": "AdHocDataSource", + } + if ds.ProviderName != nil { + node["ProviderName"] = scalarExpressionToJSON(ds.ProviderName) + } + if ds.InitString != nil { + node["InitString"] = scalarExpressionToJSON(ds.InitString) + } + return node +} + func updateStatementToJSON(s *ast.UpdateStatement) jsonNode { node := jsonNode{ "$type": "UpdateStatement", diff --git a/parser/parse_dml.go b/parser/parse_dml.go index 00ce559f..4af7ed06 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -865,20 +865,77 @@ func (p *Parser) parseExecuteSpecification() (*ast.ExecuteSpecification, error) // Parse procedure reference procRef := &ast.ExecutableProcedureReference{} + // Check for OPENDATASOURCE or OPENROWSET + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "OPENDATASOURCE" || upperLit == "OPENROWSET" { + p.nextToken() // consume OPENDATASOURCE/OPENROWSET + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + + // Parse provider name + var providerName *ast.StringLiteral + if p.curTok.Type == TokenString { + providerName = p.parseStringLiteralValue() + p.nextToken() + } + + // Expect comma + if p.curTok.Type == TokenComma { + p.nextToken() + } + + // Parse init string + var initString *ast.StringLiteral + if p.curTok.Type == TokenString { + initString = p.parseStringLiteralValue() + p.nextToken() + } + + // Expect ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + + procRef.AdHocDataSource = &ast.AdHocDataSource{ + ProviderName: providerName, + InitString: initString, + } + + // Expect . and then schema.object.procedure name + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + } + } + } + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { // Procedure variable procRef.ProcedureReference = &ast.ProcedureReferenceName{ ProcedureVariable: &ast.VariableReference{Name: p.curTok.Literal}, } p.nextToken() - } else { + } else if p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon { // Procedure name son, err := p.parseSchemaObjectName() if err != nil { return nil, err } + pr := &ast.ProcedureReference{Name: son} + + // Check for procedure number: ;number + if p.curTok.Type == TokenSemicolon { + p.nextToken() // consume ; + if p.curTok.Type == TokenNumber { + pr.Number = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + } + procRef.ProcedureReference = &ast.ProcedureReferenceName{ - ProcedureReference: &ast.ProcedureReference{Name: son}, + ProcedureReference: pr, } } @@ -1015,11 +1072,83 @@ func (p *Parser) parseExecuteContextForSpec() (*ast.ExecuteContext, error) { func (p *Parser) parseExecuteParameter() (*ast.ExecuteParameter, error) { param := &ast.ExecuteParameter{IsOutput: false} - expr, err := p.parseScalarExpression() - if err != nil { - return nil, err + // Check for DEFAULT keyword + if strings.ToUpper(p.curTok.Literal) == "DEFAULT" { + param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"} + p.nextToken() + return param, nil + } + + // Check for named parameter: @name = value + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + varName := p.curTok.Literal + p.nextToken() + + if p.curTok.Type == TokenEquals { + // Named parameter + p.nextToken() // consume = + param.Variable = &ast.VariableReference{Name: varName} + + // Check for DEFAULT keyword as value + if strings.ToUpper(p.curTok.Literal) == "DEFAULT" { + param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"} + p.nextToken() + } else { + // Parse the parameter value + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } else { + // Just a variable as value (not a named parameter) + param.ParameterValue = &ast.VariableReference{Name: varName} + } + } else { + // Check for bare identifier as IdentifierLiteral (e.g., EXEC sp_addtype birthday, datetime) + // Only if it's not followed by . or ( which would indicate a column/function reference + if p.curTok.Type == TokenIdent && !strings.HasPrefix(p.curTok.Literal, "@") { + upper := strings.ToUpper(p.curTok.Literal) + // Skip keywords that are expression starters + isKeyword := upper == "NULL" || upper == "DEFAULT" || upper == "NOT" || + upper == "CASE" || upper == "EXISTS" || upper == "CAST" || + upper == "CONVERT" || upper == "COALESCE" || upper == "NULLIF" + if !isKeyword && p.peekTok.Type != TokenDot && p.peekTok.Type != TokenLParen { + // Plain identifier - treat as IdentifierLiteral + quoteType := "NotQuoted" + if strings.HasPrefix(p.curTok.Literal, "[") { + quoteType = "SquareBracket" + } + param.ParameterValue = &ast.IdentifierLiteral{ + LiteralType: "Identifier", + QuoteType: quoteType, + Value: p.curTok.Literal, + } + p.nextToken() + } else { + // Regular value expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } else { + // Regular value expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } + + // Check for OUTPUT modifier + if strings.ToUpper(p.curTok.Literal) == "OUTPUT" || strings.ToUpper(p.curTok.Literal) == "OUT" { + param.IsOutput = true + p.nextToken() } - param.ParameterValue = expr return param, nil } diff --git a/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json b/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 994ebcfed23766d4f0bd0fe2d8b3207af1b3a3f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:45:27 +0000 Subject: [PATCH 23/29] Add computed column constraints and XML CONTENT/DOCUMENT parsing - Allow computed columns with PERSISTED to have inline constraints (NOT NULL, CHECK, FOREIGN KEY, PRIMARY KEY) - Add FOREIGN KEY inline constraint parsing for columns - Add ForeignKeyConstraintDefinition.constraintDefinition() method - Use parseFileGroupOrPartitionScheme for CREATE TABLE ON clause (supports partition scheme column lists) - Parse XML CONTENT/DOCUMENT directive as XmlDataTypeOption Enables Baselines90_CreateTableTests90 --- ast/create_table_statement.go | 1 + parser/marshal.go | 33 ++++++++++++------- parser/parse_statements.go | 12 ++++++- .../metadata.json | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/ast/create_table_statement.go b/ast/create_table_statement.go index 22b314ea..010ef69e 100644 --- a/ast/create_table_statement.go +++ b/ast/create_table_statement.go @@ -175,3 +175,4 @@ type ForeignKeyConstraintDefinition struct { func (f *ForeignKeyConstraintDefinition) node() {} func (f *ForeignKeyConstraintDefinition) tableConstraint() {} +func (f *ForeignKeyConstraintDefinition) constraintDefinition() {} diff --git a/parser/marshal.go b/parser/marshal.go index d3ce6adf..cfd13985 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -3308,14 +3308,12 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) upperLit := strings.ToUpper(p.curTok.Literal) if p.curTok.Type == TokenOn { p.nextToken() // consume ON - // Parse filegroup identifier - ident := p.parseIdentifier() - stmt.OnFileGroupOrPartitionScheme = &ast.FileGroupOrPartitionScheme{ - Name: &ast.IdentifierOrValueExpression{ - Value: ident.Value, - Identifier: ident, - }, + // Parse filegroup or partition scheme with optional columns + fg, err := p.parseFileGroupOrPartitionScheme() + if err != nil { + return nil, err } + stmt.OnFileGroupOrPartitionScheme = fg } else if upperLit == "TEXTIMAGE_ON" { p.nextToken() // consume TEXTIMAGE_ON // Parse filegroup identifier or string literal @@ -3522,10 +3520,9 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { col.IsPersisted = true p.nextToken() // consume PERSISTED } - return col, nil - } - - // Parse data type - be lenient if no data type is provided + // Fall through to parse constraints (NOT NULL, CHECK, FOREIGN KEY, etc.) + } else { + // Parse data type - be lenient if no data type is provided dataType, err := p.parseDataTypeReference() if err != nil { // Lenient: return column definition without data type @@ -3582,7 +3579,8 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { } col.IdentityOptions = identityOpts - } + } + } // end of else block for non-computed columns // Parse column constraints (NULL, NOT NULL, UNIQUE, PRIMARY KEY, DEFAULT, CHECK, CONSTRAINT) var constraintName *ast.Identifier @@ -3697,6 +3695,15 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { CheckCondition: cond, }) } + } else if upperLit == "FOREIGN" { + // Parse FOREIGN KEY constraint for column + constraint, err := p.parseForeignKeyConstraint() + if err != nil { + return nil, err + } + constraint.ConstraintIdentifier = constraintName + constraintName = nil + col.Constraints = append(col.Constraints, constraint) } else if upperLit == "CONSTRAINT" { p.nextToken() // consume CONSTRAINT // Parse and save constraint name for next constraint @@ -5197,6 +5204,8 @@ func constraintDefinitionToJSON(c ast.ConstraintDefinition) jsonNode { return uniqueConstraintToJSON(constraint) case *ast.CheckConstraintDefinition: return checkConstraintToJSON(constraint) + case *ast.ForeignKeyConstraintDefinition: + return foreignKeyConstraintToJSON(constraint) default: return jsonNode{"$type": "UnknownConstraint"} } diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 6fb13cb1..8b9b1f3a 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -694,10 +694,20 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { XmlDataTypeOption: "None", Name: baseName, } - // Check for schema collection: XML(schema_collection) + // Check for schema collection: XML(CONTENT|DOCUMENT schema_collection) if p.curTok.Type == TokenLParen { p.nextToken() // consume ( + // Check for CONTENT or DOCUMENT keyword + upper := strings.ToUpper(p.curTok.Literal) + if upper == "CONTENT" { + xmlRef.XmlDataTypeOption = "Content" + p.nextToken() + } else if upper == "DOCUMENT" { + xmlRef.XmlDataTypeOption = "Document" + p.nextToken() + } + // Parse the schema collection name schemaName, err := p.parseSchemaObjectName() if err != nil { diff --git a/parser/testdata/Baselines90_CreateTableTests90/metadata.json b/parser/testdata/Baselines90_CreateTableTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_CreateTableTests90/metadata.json +++ b/parser/testdata/Baselines90_CreateTableTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 85a7a86591a53c2dc9e1eda70dfaf84cbe8ffc59 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:48:40 +0000 Subject: [PATCH 24/29] Add UTF-16 LE BOM support in lexer Convert UTF-16 LE encoded input files to UTF-8 when the BOM (0xFF 0xFE) is detected at the start of the input. This allows parsing of test files that are encoded in UTF-16 Little Endian format. Enables AlterProcedureStatementTests --- parser/lexer.go | 21 +++++++++++++++++++ .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- parser/testdata/IdentifierTests/metadata.json | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/parser/lexer.go b/parser/lexer.go index 51bf5a0a..507f9455 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -1,8 +1,10 @@ package parser import ( + "encoding/binary" "strings" "unicode" + "unicode/utf16" ) // TokenType represents the type of a token. @@ -243,6 +245,10 @@ type Lexer struct { // NewLexer creates a new Lexer for the given input. func NewLexer(input string) *Lexer { + // Handle UTF-16 LE BOM (0xFF 0xFE) - convert to UTF-8 + if len(input) >= 2 && input[0] == 0xFF && input[1] == 0xFE { + input = utf16LEToUTF8(input[2:]) + } // Skip UTF-8 BOM if present if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF { input = input[3:] @@ -252,6 +258,21 @@ func NewLexer(input string) *Lexer { return l } +// utf16LEToUTF8 converts a UTF-16 LE string to UTF-8 +func utf16LEToUTF8(data string) string { + // Convert byte string to []uint16 + if len(data)%2 != 0 { + data = data[:len(data)-1] // Truncate odd byte + } + u16s := make([]uint16, len(data)/2) + for i := 0; i < len(u16s); i++ { + u16s[i] = binary.LittleEndian.Uint16([]byte(data[i*2 : i*2+2])) + } + // Decode UTF-16 to runes + runes := utf16.Decode(u16s) + return string(runes) +} + func (l *Lexer) readChar() { if l.readPos >= len(l.input) { l.ch = 0 diff --git a/parser/testdata/AlterProcedureStatementTests/metadata.json b/parser/testdata/AlterProcedureStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterProcedureStatementTests/metadata.json +++ b/parser/testdata/AlterProcedureStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateProcedureStatementTests160/metadata.json b/parser/testdata/CreateProcedureStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateProcedureStatementTests160/metadata.json +++ b/parser/testdata/CreateProcedureStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateProcedureStatementTests90/metadata.json b/parser/testdata/CreateProcedureStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateProcedureStatementTests90/metadata.json +++ b/parser/testdata/CreateProcedureStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/IdentifierTests/metadata.json b/parser/testdata/IdentifierTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/IdentifierTests/metadata.json +++ b/parser/testdata/IdentifierTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From c9defb8175325c2ece7de30751fd6e07ab0feeab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:52:10 +0000 Subject: [PATCH 25/29] Add TRIM function FROM keyword and function COLLATE support - Handle TRIM(chars FROM string) syntax where FROM acts as parameter separator instead of comma - Add COLLATE clause support for function calls in parsePostExpressionAccess Enables Baselines140_TrimBuiltInTest140 --- parser/parse_select.go | 14 ++++++++++++++ .../Baselines140_TrimBuiltInTest140/metadata.json | 2 +- parser/testdata/TrimBuiltInTest140/metadata.json | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/parser/parse_select.go b/parser/parse_select.go index b0c92af4..7b8308b0 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -1384,6 +1384,7 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) } // Parse parameters + funcNameUpper := strings.ToUpper(fc.FunctionName.Value) if p.curTok.Type != TokenRParen { for { param, err := p.parseScalarExpression() @@ -1392,6 +1393,12 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) } fc.Parameters = append(fc.Parameters, param) + // Special handling for TRIM function: FROM keyword acts as separator + if funcNameUpper == "TRIM" && strings.ToUpper(p.curTok.Literal) == "FROM" { + p.nextToken() // consume FROM + continue + } + if p.curTok.Type != TokenComma { break } @@ -1587,6 +1594,13 @@ func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.Scala fc.OverClause = overClause } + // Check for COLLATE clause for function calls + if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + fc.Collation = p.parseIdentifier() + continue + } + break } diff --git a/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json b/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json +++ b/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/TrimBuiltInTest140/metadata.json b/parser/testdata/TrimBuiltInTest140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/TrimBuiltInTest140/metadata.json +++ b/parser/testdata/TrimBuiltInTest140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 4514c7838e59aabd3e43caa7bda8ae9967452e67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 10:56:04 +0000 Subject: [PATCH 26/29] Add WAIT_AT_LOW_PRIORITY option for DROP INDEX statement - Add WaitAtLowPriorityOption AST type for WAIT_AT_LOW_PRIORITY clause - Add LowPriorityLockWaitMaxDurationOption for MAX_DURATION = N MINUTES - Add LowPriorityLockWaitAbortAfterWaitOption for ABORT_AFTER_WAIT - Parse WAIT_AT_LOW_PRIORITY with nested options in parentheses - Add JSON marshaling for all new option types Enables DropIndexStatementTests140, Baselines140_DropIndexStatementTests140 --- ast/drop_statements.go | 31 +++++++++ parser/marshal.go | 37 ++++++++++ parser/parse_ddl.go | 69 +++++++++++++++++++ .../metadata.json | 2 +- .../DropIndexStatementTests140/metadata.json | 2 +- 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/ast/drop_statements.go b/ast/drop_statements.go index 6dea509c..5c64335c 100644 --- a/ast/drop_statements.go +++ b/ast/drop_statements.go @@ -107,6 +107,37 @@ type FileStreamOnDropIndexOption struct { func (o *FileStreamOnDropIndexOption) node() {} func (o *FileStreamOnDropIndexOption) dropIndexOption() {} +// WaitAtLowPriorityOption represents the WAIT_AT_LOW_PRIORITY option +type WaitAtLowPriorityOption struct { + Options []LowPriorityLockWaitOption + OptionKind string // WaitAtLowPriority +} + +func (o *WaitAtLowPriorityOption) node() {} +func (o *WaitAtLowPriorityOption) dropIndexOption() {} + +// LowPriorityLockWaitOption is the interface for options within WAIT_AT_LOW_PRIORITY +type LowPriorityLockWaitOption interface { + lowPriorityLockWaitOption() +} + +// LowPriorityLockWaitMaxDurationOption represents MAX_DURATION option +type LowPriorityLockWaitMaxDurationOption struct { + MaxDuration *IntegerLiteral + Unit string // Minutes or Seconds + OptionKind string // MaxDuration +} + +func (o *LowPriorityLockWaitMaxDurationOption) lowPriorityLockWaitOption() {} + +// LowPriorityLockWaitAbortAfterWaitOption represents ABORT_AFTER_WAIT option +type LowPriorityLockWaitAbortAfterWaitOption struct { + AbortAfterWait string // None, Self, Blockers + OptionKind string // AbortAfterWait +} + +func (o *LowPriorityLockWaitAbortAfterWaitOption) lowPriorityLockWaitOption() {} + // DropStatisticsStatement represents a DROP STATISTICS statement type DropStatisticsStatement struct { Objects []*SchemaObjectName diff --git a/parser/marshal.go b/parser/marshal.go index cfd13985..aeef0c05 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -10160,6 +10160,43 @@ func dropIndexOptionToJSON(opt ast.DropIndexOption) jsonNode { "CompressionLevel": o.CompressionLevel, "OptionKind": o.OptionKind, } + case *ast.WaitAtLowPriorityOption: + node := jsonNode{ + "$type": "WaitAtLowPriorityOption", + "OptionKind": o.OptionKind, + } + if len(o.Options) > 0 { + options := make([]jsonNode, len(o.Options)) + for i, opt := range o.Options { + options[i] = lowPriorityLockWaitOptionToJSON(opt) + } + node["Options"] = options + } + return node + } + return jsonNode{} +} + +func lowPriorityLockWaitOptionToJSON(opt ast.LowPriorityLockWaitOption) jsonNode { + switch o := opt.(type) { + case *ast.LowPriorityLockWaitMaxDurationOption: + node := jsonNode{ + "$type": "LowPriorityLockWaitMaxDurationOption", + "OptionKind": o.OptionKind, + } + if o.MaxDuration != nil { + node["MaxDuration"] = scalarExpressionToJSON(o.MaxDuration) + } + if o.Unit != "" { + node["Unit"] = o.Unit + } + return node + case *ast.LowPriorityLockWaitAbortAfterWaitOption: + return jsonNode{ + "$type": "LowPriorityLockWaitAbortAfterWaitOption", + "AbortAfterWait": o.AbortAfterWait, + "OptionKind": o.OptionKind, + } } return jsonNode{} } diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index 181df612..79fb1881 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -1315,6 +1315,75 @@ func (p *Parser) parseDropIndexOptions() []ast.DropIndexOption { CompressionLevel: level, OptionKind: "DataCompression", }) + case "WAIT_AT_LOW_PRIORITY": + p.nextToken() // consume WAIT_AT_LOW_PRIORITY + waitOpt := &ast.WaitAtLowPriorityOption{ + OptionKind: "WaitAtLowPriority", + } + // Parse nested options inside parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for { + optName := strings.ToUpper(p.curTok.Literal) + if optName == "MAX_DURATION" { + p.nextToken() // consume MAX_DURATION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + maxDur := &ast.LowPriorityLockWaitMaxDurationOption{ + OptionKind: "MaxDuration", + } + // Parse integer value + if p.curTok.Type == TokenNumber { + maxDur.MaxDuration = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + // Parse unit: MINUTES or SECONDS + unitUpper := strings.ToUpper(p.curTok.Literal) + if unitUpper == "MINUTES" { + maxDur.Unit = "Minutes" + p.nextToken() + } else if unitUpper == "SECONDS" { + maxDur.Unit = "Seconds" + p.nextToken() + } + waitOpt.Options = append(waitOpt.Options, maxDur) + } else if optName == "ABORT_AFTER_WAIT" { + p.nextToken() // consume ABORT_AFTER_WAIT + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + abortOpt := &ast.LowPriorityLockWaitAbortAfterWaitOption{ + OptionKind: "AbortAfterWait", + } + abortValue := strings.ToUpper(p.curTok.Literal) + switch abortValue { + case "NONE": + abortOpt.AbortAfterWait = "None" + case "SELF": + abortOpt.AbortAfterWait = "Self" + case "BLOCKERS": + abortOpt.AbortAfterWait = "Blockers" + } + p.nextToken() + waitOpt.Options = append(waitOpt.Options, abortOpt) + } else { + break + } + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + options = append(options, waitOpt) default: // Unknown option, skip p.nextToken() diff --git a/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json b/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json +++ b/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DropIndexStatementTests140/metadata.json b/parser/testdata/DropIndexStatementTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DropIndexStatementTests140/metadata.json +++ b/parser/testdata/DropIndexStatementTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 15a628fae761ee9f227651a35dbf8f2a24b4083a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 11:13:36 +0000 Subject: [PATCH 27/29] Add comprehensive data type parsing support - Add double-quoted identifier support for data types like "xml" - Add multi-word type names: CHAR VARYING -> VarChar, DOUBLE PRECISION -> Float - Add multi-part type names for user-defined types (dbo.mytype, db.schema.type) - Add parameter parsing for user-defined types (mytype(10), mytype(max)) - Add Parameters field marshaling to userDataTypeReferenceToJSON Enables ScalarDataTypeTests90 and Baselines90_ScalarDataTypeTests90 tests. --- parser/marshal.go | 7 ++ parser/parse_statements.go | 79 ++++++++++++++++++- .../metadata.json | 2 +- .../ScalarDataTypeTests90/metadata.json | 2 +- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/parser/marshal.go b/parser/marshal.go index aeef0c05..292a87cd 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -5283,6 +5283,13 @@ func userDataTypeReferenceToJSON(dt *ast.UserDataTypeReference) jsonNode { node := jsonNode{ "$type": "UserDataTypeReference", } + if len(dt.Parameters) > 0 { + params := make([]jsonNode, len(dt.Parameters)) + for i, p := range dt.Parameters { + params[i] = scalarExpressionToJSON(p) + } + node["Parameters"] = params + } if dt.Name != nil { node["Name"] = schemaObjectNameToJSON(dt.Name) } diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 8b9b1f3a..9806aa76 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -671,10 +671,13 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { var quoteType string literal := p.curTok.Literal - // Check if this is a bracketed identifier like [int] + // Check if this is a bracketed or quoted identifier if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { typeName = literal[1 : len(literal)-1] quoteType = "SquareBracket" + } else if len(literal) >= 2 && literal[0] == '"' && literal[len(literal)-1] == '"' { + typeName = literal[1 : len(literal)-1] + quoteType = "DoubleQuote" } else { typeName = literal quoteType = "NotQuoted" @@ -726,11 +729,79 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { // Check if this is a known SQL data type sqlOption, isKnownType := getSqlDataTypeOption(typeName) + // Check for multi-word types: CHAR VARYING -> VarChar, DOUBLE PRECISION -> Float + if upper := strings.ToUpper(typeName); upper == "CHAR" || upper == "DOUBLE" { + nextUpper := strings.ToUpper(p.curTok.Literal) + if upper == "CHAR" && nextUpper == "VARYING" { + sqlOption = "VarChar" + isKnownType = true + p.nextToken() // consume VARYING + } else if upper == "DOUBLE" && nextUpper == "PRECISION" { + baseName.BaseIdentifier.Value = "FLOAT" // Use FLOAT for output + sqlOption = "Float" + isKnownType = true + p.nextToken() // consume PRECISION + } + } + if !isKnownType { - // Return UserDataTypeReference for unknown types - return &ast.UserDataTypeReference{ + // Check for multi-part type name (e.g., dbo.mytype) + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + // Get the next identifier + nextIdent := p.parseIdentifier() + // Schema.Type structure + baseName.SchemaIdentifier = baseId + baseName.BaseIdentifier = nextIdent + baseName.Count = 2 + baseName.Identifiers = []*ast.Identifier{baseId, nextIdent} + + // Check for third part: database.schema.type + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + thirdIdent := p.parseIdentifier() + // Database.Schema.Type structure + baseName.DatabaseIdentifier = baseId + baseName.SchemaIdentifier = nextIdent + baseName.BaseIdentifier = thirdIdent + baseName.Count = 3 + baseName.Identifiers = []*ast.Identifier{baseId, nextIdent, thirdIdent} + } + } + + userRef := &ast.UserDataTypeReference{ Name: baseName, - }, nil + } + + // Check for parameters: mytype(10) or mytype(10, 20) or mytype(max) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + // Special case: MAX keyword + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "MAX" { + userRef.Parameters = append(userRef.Parameters, &ast.MaxLiteral{ + LiteralType: "Max", + Value: p.curTok.Literal, + }) + p.nextToken() + } else { + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + userRef.Parameters = append(userRef.Parameters, expr) + } + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + return userRef, nil } dt := &ast.SqlDataTypeReference{ diff --git a/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json b/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json +++ b/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ScalarDataTypeTests90/metadata.json b/parser/testdata/ScalarDataTypeTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ScalarDataTypeTests90/metadata.json +++ b/parser/testdata/ScalarDataTypeTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 50719c9c835b2f86dc048cfdbb98b9a70cbea831 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 11:23:37 +0000 Subject: [PATCH 28/29] 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. --- ast/for_clause.go | 43 +++++ ast/query_specification.go | 1 + parser/marshal.go | 45 +++++ parser/parse_select.go | 181 +++++++++++++++++- .../metadata.json | 2 +- parser/testdata/ForClauseTests/metadata.json | 2 +- 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 ast/for_clause.go diff --git a/ast/for_clause.go b/ast/for_clause.go new file mode 100644 index 00000000..bea8432a --- /dev/null +++ b/ast/for_clause.go @@ -0,0 +1,43 @@ +package ast + +// ForClause is an interface for different types of FOR clauses. +type ForClause interface { + Node + forClause() +} + +// BrowseForClause represents a FOR BROWSE clause. +type BrowseForClause struct{} + +func (*BrowseForClause) node() {} +func (*BrowseForClause) forClause() {} + +// ReadOnlyForClause represents a FOR READ ONLY clause. +type ReadOnlyForClause struct{} + +func (*ReadOnlyForClause) node() {} +func (*ReadOnlyForClause) forClause() {} + +// UpdateForClause represents a FOR UPDATE [OF columns] clause. +type UpdateForClause struct { + Columns []*ColumnReferenceExpression `json:"Columns,omitempty"` +} + +func (*UpdateForClause) node() {} +func (*UpdateForClause) forClause() {} + +// XmlForClause represents a FOR XML clause with its options. +type XmlForClause struct { + Options []*XmlForClauseOption `json:"Options,omitempty"` +} + +func (*XmlForClause) node() {} +func (*XmlForClause) forClause() {} + +// XmlForClauseOption represents an option in a FOR XML clause. +type XmlForClauseOption struct { + OptionKind string `json:"OptionKind,omitempty"` + Value *StringLiteral `json:"Value,omitempty"` +} + +func (*XmlForClauseOption) node() {} diff --git a/ast/query_specification.go b/ast/query_specification.go index df4b748a..1f979b53 100644 --- a/ast/query_specification.go +++ b/ast/query_specification.go @@ -10,6 +10,7 @@ type QuerySpecification struct { GroupByClause *GroupByClause `json:"GroupByClause,omitempty"` HavingClause *HavingClause `json:"HavingClause,omitempty"` OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` + ForClause ForClause `json:"ForClause,omitempty"` } func (*QuerySpecification) node() {} diff --git a/parser/marshal.go b/parser/marshal.go index 292a87cd..ade96519 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -1253,6 +1253,51 @@ func querySpecificationToJSON(q *ast.QuerySpecification) jsonNode { if q.OrderByClause != nil { node["OrderByClause"] = orderByClauseToJSON(q.OrderByClause) } + if q.ForClause != nil { + node["ForClause"] = forClauseToJSON(q.ForClause) + } + return node +} + +func forClauseToJSON(fc ast.ForClause) jsonNode { + switch f := fc.(type) { + case *ast.BrowseForClause: + return jsonNode{"$type": "BrowseForClause"} + case *ast.ReadOnlyForClause: + return jsonNode{"$type": "ReadOnlyForClause"} + case *ast.UpdateForClause: + node := jsonNode{"$type": "UpdateForClause"} + if len(f.Columns) > 0 { + cols := make([]jsonNode, len(f.Columns)) + for i, col := range f.Columns { + cols[i] = columnReferenceExpressionToJSON(col) + } + node["Columns"] = cols + } + return node + case *ast.XmlForClause: + node := jsonNode{"$type": "XmlForClause"} + if len(f.Options) > 0 { + opts := make([]jsonNode, len(f.Options)) + for i, opt := range f.Options { + opts[i] = xmlForClauseOptionToJSON(opt) + } + node["Options"] = opts + } + return node + default: + return jsonNode{"$type": "UnknownForClause"} + } +} + +func xmlForClauseOptionToJSON(opt *ast.XmlForClauseOption) jsonNode { + node := jsonNode{"$type": "XmlForClauseOption"} + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + if opt.Value != nil { + node["Value"] = stringLiteralToJSON(opt.Value) + } return node } diff --git a/parser/parse_select.go b/parser/parse_select.go index 7b8308b0..43e46b1c 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -186,6 +186,18 @@ func (p *Parser) parseQueryExpressionWithInto() (ast.QueryExpression, *ast.Schem } } + // Parse FOR clause (FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY) + if strings.ToUpper(p.curTok.Literal) == "FOR" { + forClause, err := p.parseForClause() + if err != nil { + return nil, nil, nil, err + } + // Attach to QuerySpecification + if qs, ok := left.(*ast.QuerySpecification); ok { + qs.ForClause = forClause + } + } + return left, into, on, nil } @@ -1797,7 +1809,7 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { } else if p.curTok.Type == TokenIdent { // Could be an alias without AS, but need to be careful not to consume keywords upper := strings.ToUpper(p.curTok.Literal) - 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" { + 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" { ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"} p.nextToken() } @@ -1857,7 +1869,7 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a // Could be an alias without AS, but need to be careful not to consume keywords if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - 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" { + 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" { ref.Alias = p.parseIdentifier() } } else { @@ -3436,3 +3448,168 @@ func (p *Parser) parsePredictTableReference() (*ast.PredictTableReference, error return ref, nil } + +// parseForClause parses FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY clauses. +func (p *Parser) parseForClause() (ast.ForClause, error) { + p.nextToken() // consume FOR + + keyword := strings.ToUpper(p.curTok.Literal) + + switch keyword { + case "BROWSE": + p.nextToken() // consume BROWSE + return &ast.BrowseForClause{}, nil + + case "READ": + p.nextToken() // consume READ + if strings.ToUpper(p.curTok.Literal) == "ONLY" { + p.nextToken() // consume ONLY + } + return &ast.ReadOnlyForClause{}, nil + + case "UPDATE": + p.nextToken() // consume UPDATE + clause := &ast.UpdateForClause{} + + // Check for OF column_list + if strings.ToUpper(p.curTok.Literal) == "OF" { + p.nextToken() // consume OF + + // Parse column list + for { + col, err := p.parseColumnReference() + if err != nil { + return nil, err + } + clause.Columns = append(clause.Columns, col) + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + } + return clause, nil + + case "XML": + p.nextToken() // consume XML + return p.parseXmlForClause() + + default: + return nil, fmt.Errorf("unexpected token after FOR: %s", p.curTok.Literal) + } +} + +// parseXmlForClause parses FOR XML options. +func (p *Parser) parseXmlForClause() (*ast.XmlForClause, error) { + clause := &ast.XmlForClause{} + + // Parse XML options separated by commas + for { + option, err := p.parseXmlForClauseOption() + if err != nil { + return nil, err + } + clause.Options = append(clause.Options, option) + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + + return clause, nil +} + +// parseXmlForClauseOption parses a single XML FOR clause option. +func (p *Parser) parseXmlForClauseOption() (*ast.XmlForClauseOption, error) { + option := &ast.XmlForClauseOption{} + + keyword := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume the option keyword + + switch keyword { + case "AUTO": + option.OptionKind = "Auto" + case "EXPLICIT": + option.OptionKind = "Explicit" + case "RAW": + option.OptionKind = "Raw" + // Check for optional element name: RAW ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "PATH": + option.OptionKind = "Path" + // Check for optional path name: PATH ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "ELEMENTS": + // Check for XSINIL or ABSENT + nextKeyword := strings.ToUpper(p.curTok.Literal) + if nextKeyword == "XSINIL" { + option.OptionKind = "ElementsXsiNil" + p.nextToken() // consume XSINIL + } else if nextKeyword == "ABSENT" { + option.OptionKind = "ElementsAbsent" + p.nextToken() // consume ABSENT + } else { + option.OptionKind = "Elements" + } + case "XMLDATA": + option.OptionKind = "XmlData" + case "XMLSCHEMA": + option.OptionKind = "XmlSchema" + // Check for optional namespace: XMLSCHEMA ('namespace') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "ROOT": + option.OptionKind = "Root" + // Check for optional root name: ROOT ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "TYPE": + option.OptionKind = "Type" + case "BINARY": + // BINARY BASE64 + if strings.ToUpper(p.curTok.Literal) == "BASE64" { + option.OptionKind = "BinaryBase64" + p.nextToken() // consume BASE64 + } + default: + option.OptionKind = keyword + } + + return option, nil +} diff --git a/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json b/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ForClauseTests/metadata.json b/parser/testdata/ForClauseTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ForClauseTests/metadata.json +++ b/parser/testdata/ForClauseTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} From 81abf4bfb054a3c3ac2a1af3653d74fe14143c81 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 11:30:55 +0000 Subject: [PATCH 29/29] Add comprehensive CREATE EXTERNAL TABLE parsing support - Update CreateExternalTableStatement AST with ColumnDefinitions and ExternalTableOptions fields - Add ExternalTableColumnDefinition type with ColumnDefinition and NullableConstraint - Add ExternalTableLiteralOrIdentifierOption type for table options - Parse column definitions with data types, collation, and NULL/NOT NULL - Parse WITH clause options: DATA_SOURCE, LOCATION, FILE_FORMAT, TABLE_OPTIONS - Handle national string literals (N'...') properly in option values - Add Collation field to ColumnDefinitionBase - Add JSON marshaling for new types Enables multiple CREATE EXTERNAL TABLE tests: - Baselines160_CreateExternalTableStatementTests160 - CreateExternalTableStatementTests160 - CreateExternalTableStatementTestsFabricDW - BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW --- ast/bulk_insert_statement.go | 3 +- ast/external_statements.go | 24 +++- parser/marshal.go | 46 +++++++ parser/parse_statements.go | 124 +++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- 8 files changed, 189 insertions(+), 16 deletions(-) diff --git a/ast/bulk_insert_statement.go b/ast/bulk_insert_statement.go index a9780cbf..a0803e2f 100644 --- a/ast/bulk_insert_statement.go +++ b/ast/bulk_insert_statement.go @@ -28,8 +28,9 @@ type InsertBulkColumnDefinition struct { // ColumnDefinitionBase represents a basic column definition. type ColumnDefinitionBase struct { - ColumnIdentifier *Identifier `json:"ColumnIdentifier,omitempty"` + ColumnIdentifier *Identifier `json:"ColumnIdentifier,omitempty"` DataType DataTypeReference `json:"DataType,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } // BulkInsertOption is the interface for bulk insert options. diff --git a/ast/external_statements.go b/ast/external_statements.go index b73b3ddc..f166fbbf 100644 --- a/ast/external_statements.go +++ b/ast/external_statements.go @@ -50,18 +50,28 @@ func (o *ExternalFileFormatLiteralOption) externalFileFormatOption() {} // CreateExternalTableStatement represents CREATE EXTERNAL TABLE statement type CreateExternalTableStatement struct { - SchemaObjectName *SchemaObjectName - Definition *TableDefinition - DataSource *Identifier - Location ScalarExpression - FileFormat *Identifier - Options []*ExternalTableOption + SchemaObjectName *SchemaObjectName + ColumnDefinitions []*ExternalTableColumnDefinition + DataSource *Identifier + ExternalTableOptions []*ExternalTableLiteralOrIdentifierOption } func (s *CreateExternalTableStatement) node() {} func (s *CreateExternalTableStatement) statement() {} -// ExternalTableOption represents an option for external table +// ExternalTableColumnDefinition represents a column definition in an external table +type ExternalTableColumnDefinition struct { + ColumnDefinition *ColumnDefinitionBase + NullableConstraint *NullableConstraintDefinition +} + +// ExternalTableLiteralOrIdentifierOption represents an option for external table +type ExternalTableLiteralOrIdentifierOption struct { + OptionKind string + Value *IdentifierOrValueExpression +} + +// ExternalTableOption represents a simple option for external table (legacy) type ExternalTableOption struct { OptionKind string Value ScalarExpression diff --git a/parser/marshal.go b/parser/marshal.go index ade96519..d7699ca9 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -11019,6 +11019,49 @@ func createExternalTableStatementToJSON(s *ast.CreateExternalTableStatement) jso if s.SchemaObjectName != nil { node["SchemaObjectName"] = schemaObjectNameToJSON(s.SchemaObjectName) } + if len(s.ColumnDefinitions) > 0 { + cols := make([]jsonNode, len(s.ColumnDefinitions)) + for i, col := range s.ColumnDefinitions { + cols[i] = externalTableColumnDefinitionToJSON(col) + } + node["ColumnDefinitions"] = cols + } + if s.DataSource != nil { + node["DataSource"] = identifierToJSON(s.DataSource) + } + if len(s.ExternalTableOptions) > 0 { + opts := make([]jsonNode, len(s.ExternalTableOptions)) + for i, opt := range s.ExternalTableOptions { + opts[i] = externalTableLiteralOrIdentifierOptionToJSON(opt) + } + node["ExternalTableOptions"] = opts + } + return node +} + +func externalTableColumnDefinitionToJSON(col *ast.ExternalTableColumnDefinition) jsonNode { + node := jsonNode{ + "$type": "ExternalTableColumnDefinition", + } + if col.ColumnDefinition != nil { + node["ColumnDefinition"] = columnDefinitionBaseToJSON(col.ColumnDefinition) + } + if col.NullableConstraint != nil { + node["NullableConstraint"] = nullableConstraintToJSON(col.NullableConstraint) + } + return node +} + +func externalTableLiteralOrIdentifierOptionToJSON(opt *ast.ExternalTableLiteralOrIdentifierOption) jsonNode { + node := jsonNode{ + "$type": "ExternalTableLiteralOrIdentifierOption", + } + if opt.Value != nil { + node["Value"] = identifierOrValueExpressionToJSON(opt.Value) + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } return node } @@ -11265,6 +11308,9 @@ func columnDefinitionBaseToJSON(c *ast.ColumnDefinitionBase) jsonNode { if c.DataType != nil { node["DataType"] = dataTypeReferenceToJSON(c.DataType) } + if c.Collation != nil { + node["Collation"] = identifierToJSON(c.Collation) + } return node } diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 9806aa76..d674711f 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -6439,7 +6439,6 @@ func (p *Parser) externalFileFormatOptionKind(name string) string { } func (p *Parser) parseCreateExternalTableStatement() (*ast.CreateExternalTableStatement, error) { - // TABLE name - skip rest of statement for now p.nextToken() // consume TABLE name, err := p.parseSchemaObjectName() @@ -6450,16 +6449,133 @@ func (p *Parser) parseCreateExternalTableStatement() (*ast.CreateExternalTableSt SchemaObjectName: name, } - // Skip rest of statement - for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { - p.nextToken() + // Parse column definitions in parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef, err := p.parseExternalTableColumnDefinition() + if err != nil { + return nil, err + } + stmt.ColumnDefinitions = append(stmt.ColumnDefinitions, colDef) + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + // 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 { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + switch optName { + case "DATA_SOURCE": + stmt.DataSource = p.parseIdentifier() + case "LOCATION", "FILE_FORMAT", "TABLE_OPTIONS": + opt := &ast.ExternalTableLiteralOrIdentifierOption{ + Value: &ast.IdentifierOrValueExpression{}, + } + switch optName { + case "LOCATION": + opt.OptionKind = "Location" + case "FILE_FORMAT": + opt.OptionKind = "FileFormat" + case "TABLE_OPTIONS": + opt.OptionKind = "TableOptions" + } + + // Parse the value (can be identifier or string literal) + if p.curTok.Type == TokenString { + strLit := p.parseStringLiteralValue() + p.nextToken() // consume string + opt.Value.Value = strLit.Value + opt.Value.ValueExpression = strLit + } else if p.curTok.Type == TokenNationalString { + strLit, _ := p.parseNationalStringFromToken() + opt.Value.Value = strLit.Value + opt.Value.ValueExpression = strLit + } else if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + ident := p.parseIdentifier() + opt.Value.Value = ident.Value + opt.Value.Identifier = ident + } + stmt.ExternalTableOptions = append(stmt.ExternalTableOptions, opt) + default: + // Skip unknown options + for p.curTok.Type != TokenComma && p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } } + if p.curTok.Type == TokenSemicolon { p.nextToken() } return stmt, nil } +func (p *Parser) parseExternalTableColumnDefinition() (*ast.ExternalTableColumnDefinition, error) { + colDef := &ast.ExternalTableColumnDefinition{ + ColumnDefinition: &ast.ColumnDefinitionBase{}, + } + + // Parse column name + colDef.ColumnDefinition.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + dt, err := p.parseDataType() + if err != nil { + return nil, err + } + colDef.ColumnDefinition.DataType = dt + + // Parse optional COLLATE + if strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + colDef.ColumnDefinition.Collation = p.parseIdentifier() + } + + // Parse optional NULL/NOT NULL + if strings.ToUpper(p.curTok.Literal) == "NOT" { + p.nextToken() // consume NOT + if strings.ToUpper(p.curTok.Literal) == "NULL" { + p.nextToken() // consume NULL + colDef.NullableConstraint = &ast.NullableConstraintDefinition{ + Nullable: false, + } + } + } else if strings.ToUpper(p.curTok.Literal) == "NULL" { + p.nextToken() // consume NULL + colDef.NullableConstraint = &ast.NullableConstraintDefinition{ + Nullable: true, + } + } + + return colDef, nil +} + func (p *Parser) parseCreateExternalLanguageStatement() (*ast.CreateExternalLanguageStatement, error) { p.nextToken() // consume LANGUAGE stmt := &ast.CreateExternalLanguageStatement{ diff --git a/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json b/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json b/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json +++ b/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateExternalTableStatementTests160/metadata.json b/parser/testdata/CreateExternalTableStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateExternalTableStatementTests160/metadata.json +++ b/parser/testdata/CreateExternalTableStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json b/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json +++ b/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{}