diff --git a/CLAUDE.md b/CLAUDE.md index 4dc1610c..2680fdbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,3 +60,7 @@ Each test in `parser/testdata/` contains: - `metadata.json` - `{"skip": true}` or `{"skip": false}` - `query.sql` - T-SQL to parse - `ast.json` - Expected AST output + +## Important Rules + +- **NEVER modify `ast.json` files** - These are golden files containing the expected output. If tests fail due to JSON mismatches, fix the Go code to match the expected output, not the other way around. diff --git a/ast/select_set_variable.go b/ast/select_set_variable.go new file mode 100644 index 00000000..dad0188d --- /dev/null +++ b/ast/select_set_variable.go @@ -0,0 +1,12 @@ +package ast + +// SelectSetVariable represents a variable assignment in a SELECT statement. +// Example: SELECT @a = 1, @b ||= 'foo' +type SelectSetVariable struct { + Variable *VariableReference `json:"Variable,omitempty"` + Expression ScalarExpression `json:"Expression,omitempty"` + AssignmentKind string `json:"AssignmentKind,omitempty"` +} + +func (*SelectSetVariable) node() {} +func (*SelectSetVariable) selectElement() {} diff --git a/ast/workload_classifier_statement.go b/ast/workload_classifier_statement.go new file mode 100644 index 00000000..5524925b --- /dev/null +++ b/ast/workload_classifier_statement.go @@ -0,0 +1,87 @@ +// Package ast provides AST types for T-SQL parsing. +package ast + +// CreateWorkloadClassifierStatement represents a CREATE WORKLOAD CLASSIFIER statement. +type CreateWorkloadClassifierStatement struct { + ClassifierName *Identifier + Options []WorkloadClassifierOption +} + +func (s *CreateWorkloadClassifierStatement) statement() {} +func (s *CreateWorkloadClassifierStatement) node() {} + +// WorkloadClassifierOption is the interface for workload classifier options. +type WorkloadClassifierOption interface { + node() + workloadClassifierOption() +} + +// ClassifierWorkloadGroupOption represents the WORKLOAD_GROUP option. +type ClassifierWorkloadGroupOption struct { + WorkloadGroupName *StringLiteral + OptionType string +} + +func (o *ClassifierWorkloadGroupOption) node() {} +func (o *ClassifierWorkloadGroupOption) workloadClassifierOption() {} + +// ClassifierMemberNameOption represents the MEMBERNAME option. +type ClassifierMemberNameOption struct { + MemberName *StringLiteral + OptionType string +} + +func (o *ClassifierMemberNameOption) node() {} +func (o *ClassifierMemberNameOption) workloadClassifierOption() {} + +// ClassifierWlmContextOption represents the WLM_CONTEXT option. +type ClassifierWlmContextOption struct { + WlmContext *StringLiteral + OptionType string +} + +func (o *ClassifierWlmContextOption) node() {} +func (o *ClassifierWlmContextOption) workloadClassifierOption() {} + +// WlmTimeLiteral represents a time literal for WLM START_TIME/END_TIME options. +type WlmTimeLiteral struct { + TimeString *StringLiteral +} + +func (t *WlmTimeLiteral) node() {} + +// ClassifierStartTimeOption represents the START_TIME option. +type ClassifierStartTimeOption struct { + Time *WlmTimeLiteral + OptionType string +} + +func (o *ClassifierStartTimeOption) node() {} +func (o *ClassifierStartTimeOption) workloadClassifierOption() {} + +// ClassifierEndTimeOption represents the END_TIME option. +type ClassifierEndTimeOption struct { + Time *WlmTimeLiteral + OptionType string +} + +func (o *ClassifierEndTimeOption) node() {} +func (o *ClassifierEndTimeOption) workloadClassifierOption() {} + +// ClassifierWlmLabelOption represents the WLM_LABEL option. +type ClassifierWlmLabelOption struct { + WlmLabel *StringLiteral + OptionType string +} + +func (o *ClassifierWlmLabelOption) node() {} +func (o *ClassifierWlmLabelOption) workloadClassifierOption() {} + +// ClassifierImportanceOption represents the IMPORTANCE option. +type ClassifierImportanceOption struct { + Importance string + OptionType string +} + +func (o *ClassifierImportanceOption) node() {} +func (o *ClassifierImportanceOption) workloadClassifierOption() {} diff --git a/parser/lexer.go b/parser/lexer.go index e5d8089e..2ca5e243 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -85,6 +85,19 @@ const ( TokenRBrace TokenLeftShift TokenRightShift + TokenPipe // | + TokenDoublePipe // || + TokenConcatEquals // ||= + TokenBitwiseAnd // & + TokenPlusEquals // += + TokenMinusEquals // -= + TokenStarEquals // *= + TokenSlashEquals // /= + TokenModuloEquals // %= + TokenAndEquals // &= + TokenOrEquals // |= + TokenXorEquals // ^= + TokenCaret // ^ // DML Keywords TokenInsert @@ -265,9 +278,16 @@ func (l *Lexer) NextToken() Token { tok.Type = TokenEOF tok.Literal = "" case '*': - tok.Type = TokenStar - tok.Literal = "*" - l.readChar() + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenStarEquals + tok.Literal = "*=" + l.readChar() + } else { + tok.Type = TokenStar + tok.Literal = "*" + l.readChar() + } case ',': tok.Type = TokenComma tok.Literal = "," @@ -355,21 +375,94 @@ func (l *Lexer) NextToken() Token { tok.Literal = "}" l.readChar() case '+': - tok.Type = TokenPlus - tok.Literal = "+" - l.readChar() + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenPlusEquals + tok.Literal = "+=" + l.readChar() + } else { + tok.Type = TokenPlus + tok.Literal = "+" + l.readChar() + } case '-': - tok.Type = TokenMinus - tok.Literal = "-" - l.readChar() + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenMinusEquals + tok.Literal = "-=" + l.readChar() + } else { + tok.Type = TokenMinus + tok.Literal = "-" + l.readChar() + } case '/': - tok.Type = TokenSlash - tok.Literal = "/" - l.readChar() + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenSlashEquals + tok.Literal = "/=" + l.readChar() + } else { + tok.Type = TokenSlash + tok.Literal = "/" + l.readChar() + } case '%': - tok.Type = TokenModulo - tok.Literal = "%" - l.readChar() + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenModuloEquals + tok.Literal = "%=" + l.readChar() + } else { + tok.Type = TokenModulo + tok.Literal = "%" + l.readChar() + } + case '|': + if l.peekChar() == '|' { + l.readChar() // consume first | + if l.peekChar() == '=' { + l.readChar() // consume second | + tok.Type = TokenConcatEquals + tok.Literal = "||=" + l.readChar() // consume = + } else { + tok.Type = TokenDoublePipe + tok.Literal = "||" + l.readChar() // consume second | + } + } else if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenOrEquals + tok.Literal = "|=" + l.readChar() + } else { + tok.Type = TokenPipe + tok.Literal = "|" + l.readChar() + } + case '&': + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenAndEquals + tok.Literal = "&=" + l.readChar() + } else { + tok.Type = TokenBitwiseAnd + tok.Literal = "&" + l.readChar() + } + case '^': + if l.peekChar() == '=' { + l.readChar() + tok.Type = TokenXorEquals + tok.Literal = "^=" + l.readChar() + } else { + tok.Type = TokenCaret + tok.Literal = "^" + l.readChar() + } case '\'': tok = l.readString() default: diff --git a/parser/marshal.go b/parser/marshal.go index 34b22a0b..b9b460ff 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -188,6 +188,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropWorkloadClassifierStatementToJSON(s) case *ast.CreateWorkloadGroupStatement: return createWorkloadGroupStatementToJSON(s) + case *ast.CreateWorkloadClassifierStatement: + return createWorkloadClassifierStatementToJSON(s) case *ast.AlterWorkloadGroupStatement: return alterWorkloadGroupStatementToJSON(s) case *ast.AlterSequenceStatement: @@ -1055,6 +1057,26 @@ func selectElementToJSON(elem ast.SelectElement) jsonNode { node["Qualifier"] = multiPartIdentifierToJSON(e.Qualifier) } return node + case *ast.SelectSetVariable: + node := jsonNode{ + "$type": "SelectSetVariable", + } + if e.Variable != nil { + varNode := jsonNode{ + "$type": "VariableReference", + } + if e.Variable.Name != "" { + varNode["Name"] = e.Variable.Name + } + node["Variable"] = varNode + } + if e.Expression != nil { + node["Expression"] = scalarExpressionToJSON(e.Expression) + } + if e.AssignmentKind != "" { + node["AssignmentKind"] = e.AssignmentKind + } + return node default: return jsonNode{"$type": "UnknownSelectElement"} } @@ -1094,9 +1116,8 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { // Always include IsNational and IsLargeObject node["IsNational"] = e.IsNational node["IsLargeObject"] = e.IsLargeObject - if e.Value != "" { - node["Value"] = e.Value - } + // Always include Value for StringLiteral, even if empty + node["Value"] = e.Value return node case *ast.BinaryLiteral: node := jsonNode{ @@ -2065,6 +2086,8 @@ func setVariableStatementToJSON(s *ast.SetVariableStatement) jsonNode { } if s.SeparatorType != "" { node["SeparatorType"] = s.SeparatorType + } else { + node["SeparatorType"] = "NotSpecified" } if s.Identifier != nil { node["Identifier"] = identifierToJSON(s.Identifier) @@ -5572,6 +5595,116 @@ func alterWorkloadGroupStatementToJSON(s *ast.AlterWorkloadGroupStatement) jsonN return node } +func createWorkloadClassifierStatementToJSON(s *ast.CreateWorkloadClassifierStatement) jsonNode { + node := jsonNode{ + "$type": "CreateWorkloadClassifierStatement", + } + if s.ClassifierName != nil { + node["ClassifierName"] = identifierToJSON(s.ClassifierName) + } + if len(s.Options) > 0 { + opts := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + opts[i] = workloadClassifierOptionToJSON(opt) + } + node["Options"] = opts + } + return node +} + +func stringLiteralToJSON(s *ast.StringLiteral) jsonNode { + node := jsonNode{ + "$type": "StringLiteral", + } + if s.LiteralType != "" { + node["LiteralType"] = s.LiteralType + } else { + node["LiteralType"] = "String" + } + node["IsNational"] = s.IsNational + node["IsLargeObject"] = s.IsLargeObject + // Always include Value for StringLiteral, even if empty + node["Value"] = s.Value + return node +} + +func workloadClassifierOptionToJSON(opt ast.WorkloadClassifierOption) jsonNode { + switch o := opt.(type) { + case *ast.ClassifierWorkloadGroupOption: + node := jsonNode{ + "$type": "ClassifierWorkloadGroupOption", + "OptionType": o.OptionType, + } + if o.WorkloadGroupName != nil { + node["WorkloadGroupName"] = stringLiteralToJSON(o.WorkloadGroupName) + } + return node + case *ast.ClassifierMemberNameOption: + node := jsonNode{ + "$type": "ClassifierMemberNameOption", + "OptionType": o.OptionType, + } + if o.MemberName != nil { + node["MemberName"] = stringLiteralToJSON(o.MemberName) + } + return node + case *ast.ClassifierWlmContextOption: + node := jsonNode{ + "$type": "ClassifierWlmContextOption", + "OptionType": o.OptionType, + } + if o.WlmContext != nil { + node["WlmContext"] = stringLiteralToJSON(o.WlmContext) + } + return node + case *ast.ClassifierStartTimeOption: + node := jsonNode{ + "$type": "ClassifierStartTimeOption", + "OptionType": o.OptionType, + } + if o.Time != nil { + node["Time"] = wlmTimeLiteralToJSON(o.Time) + } + return node + case *ast.ClassifierEndTimeOption: + node := jsonNode{ + "$type": "ClassifierEndTimeOption", + "OptionType": o.OptionType, + } + if o.Time != nil { + node["Time"] = wlmTimeLiteralToJSON(o.Time) + } + return node + case *ast.ClassifierWlmLabelOption: + node := jsonNode{ + "$type": "ClassifierWlmLabelOption", + "OptionType": o.OptionType, + } + if o.WlmLabel != nil { + node["WlmLabel"] = stringLiteralToJSON(o.WlmLabel) + } + return node + case *ast.ClassifierImportanceOption: + return jsonNode{ + "$type": "ClassifierImportanceOption", + "Importance": o.Importance, + "OptionType": o.OptionType, + } + default: + return jsonNode{} + } +} + +func wlmTimeLiteralToJSON(t *ast.WlmTimeLiteral) jsonNode { + node := jsonNode{ + "$type": "WlmTimeLiteral", + } + if t.TimeString != nil { + node["TimeString"] = stringLiteralToJSON(t.TimeString) + } + return node +} + func workloadGroupParameterToJSON(p interface{}) jsonNode { switch param := p.(type) { case *ast.WorkloadGroupResourceParameter: diff --git a/parser/parse_dml.go b/parser/parse_dml.go index b071f7dc..d13705ac 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -548,26 +548,66 @@ func (p *Parser) parseSetClauses() ([]ast.SetClause, error) { return clauses, nil } +// isCompoundAssignment checks if the current token is a compound assignment operator +func (p *Parser) isCompoundAssignment() bool { + switch p.curTok.Type { + case TokenEquals, TokenConcatEquals, TokenPlusEquals, TokenMinusEquals, + TokenStarEquals, TokenSlashEquals, TokenModuloEquals, + TokenAndEquals, TokenOrEquals, TokenXorEquals: + return true + } + return false +} + +// getAssignmentKind returns the AssignmentKind for the current compound assignment token +func (p *Parser) getAssignmentKind() string { + switch p.curTok.Type { + case TokenEquals: + return "Equals" + case TokenConcatEquals: + return "ConcatEquals" + case TokenPlusEquals: + return "AddEquals" + case TokenMinusEquals: + return "SubtractEquals" + case TokenStarEquals: + return "MultiplyEquals" + case TokenSlashEquals: + return "DivideEquals" + case TokenModuloEquals: + return "ModEquals" + case TokenAndEquals: + return "BitwiseAndEquals" + case TokenOrEquals: + return "BitwiseOrEquals" + case TokenXorEquals: + return "BitwiseXorEquals" + } + return "Equals" +} + func (p *Parser) parseAssignmentSetClause() (*ast.AssignmentSetClause, error) { clause := &ast.AssignmentSetClause{AssignmentKind: "Equals"} - // Could be @var = col = value, @var = value, or col = value + // Could be @var = col = value, @var = value, @var ||= value, or col = value, col ||= value if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { varName := p.curTok.Literal p.nextToken() - if p.curTok.Type == TokenEquals { + if p.isCompoundAssignment() { + clause.AssignmentKind = p.getAssignmentKind() clause.Variable = &ast.VariableReference{Name: varName} p.nextToken() - // Check if next is column = value (SET @a = col = value) + // Check if next is column = value or column ||= value (SET @a = col = value) if p.curTok.Type == TokenIdent && !strings.HasPrefix(p.curTok.Literal, "@") { - // Could be @a = col = value or @a = expr + // Could be @a = col = value, @a = col ||= value or @a = expr savedTok := p.curTok col, err := p.parseMultiPartIdentifierAsColumn() if err != nil { return nil, err } - if p.curTok.Type == TokenEquals { + if p.isCompoundAssignment() { + clause.AssignmentKind = p.getAssignmentKind() clause.Column = col p.nextToken() val, err := p.parseScalarExpression() @@ -587,7 +627,7 @@ func (p *Parser) parseAssignmentSetClause() (*ast.AssignmentSetClause, error) { return clause, nil } - // Just @var = value + // Just @var = value or @var ||= value val, err := p.parseScalarExpression() if err != nil { return nil, err @@ -597,17 +637,19 @@ func (p *Parser) parseAssignmentSetClause() (*ast.AssignmentSetClause, error) { } } - // col = value + // col = value or col ||= value col, err := p.parseMultiPartIdentifierAsColumn() if err != nil { return nil, err } clause.Column = col - if p.curTok.Type != TokenEquals { + if p.isCompoundAssignment() { + clause.AssignmentKind = p.getAssignmentKind() + p.nextToken() + } else { return nil, fmt.Errorf("expected =, got %s", p.curTok.Literal) } - p.nextToken() val, err := p.parseScalarExpression() if err != nil { diff --git a/parser/parse_select.go b/parser/parse_select.go index 8eb8dd1d..9e68e3ba 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -372,6 +372,60 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { return &ast.SelectStarExpression{}, nil } + // Check for variable assignment: @var = expr or @var ||= expr + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + varName := p.curTok.Literal + p.nextToken() // consume variable + + // Check if this is an assignment + if p.isCompoundAssignment() { + ssv := &ast.SelectSetVariable{ + Variable: &ast.VariableReference{Name: varName}, + AssignmentKind: p.getAssignmentKind(), + } + p.nextToken() // consume assignment operator + + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + ssv.Expression = expr + return ssv, nil + } + + // 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 for column alias + if p.curTok.Type == TokenIdent && p.curTok.Literal[0] == '[' { + alias := p.parseIdentifier() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: alias.Value, + Identifier: alias, + } + } else if p.curTok.Type == TokenAs { + p.nextToken() + alias := p.parseIdentifier() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: alias.Value, + Identifier: alias, + } + } else if p.curTok.Type == TokenIdent { + 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" { + alias := p.parseIdentifier() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: alias.Value, + Identifier: alias, + } + } + } + return sse, nil + } + // Otherwise parse a scalar expression expr, err := p.parseScalarExpression() if err != nil { @@ -487,12 +541,15 @@ func (p *Parser) parseAdditiveExpression() (ast.ScalarExpression, error) { return nil, err } - for p.curTok.Type == TokenPlus || p.curTok.Type == TokenMinus { + for p.curTok.Type == TokenPlus || p.curTok.Type == TokenMinus || p.curTok.Type == TokenDoublePipe { var opType string - if p.curTok.Type == TokenPlus { + switch p.curTok.Type { + case TokenPlus: opType = "Add" - } else { + case TokenMinus: opType = "Subtract" + case TokenDoublePipe: + opType = "Concat" } p.nextToken() diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 6da6697c..8e1478fa 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -814,11 +814,13 @@ func (p *Parser) parseSetVariableStatement() (ast.Statement, error) { return stmt, nil } - // Expect = - if p.curTok.Type != TokenEquals { + // Expect = or compound assignment operator + if p.isCompoundAssignment() { + stmt.AssignmentKind = p.getAssignmentKind() + p.nextToken() + } else { return nil, fmt.Errorf("expected =, got %s", p.curTok.Literal) } - p.nextToken() // Check for CURSOR definition if p.curTok.Type == TokenCursor { @@ -1234,6 +1236,11 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { case "FEDERATION": return p.parseCreateFederationStatement() case "WORKLOAD": + // Check if it's CLASSIFIER or GROUP + nextWord := strings.ToUpper(p.peekTok.Literal) + if nextWord == "CLASSIFIER" { + return p.parseCreateWorkloadClassifierStatement() + } return p.parseCreateWorkloadGroupStatement() case "SEQUENCE": return p.parseCreateSequenceStatement() @@ -3568,17 +3575,52 @@ func (p *Parser) parseSendStatement() (*ast.SendStatement, error) { p.nextToken() // consume CONVERSATION // Parse conversation handle(s) - for { + // Syntax: (@var) OR (@var1, @var2, ...) OR ((@var)) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + + // Check for double parens: ((...)) + if p.curTok.Type == TokenLParen { + // Double paren case - parse as single ParenthesisExpression + p.nextToken() // consume inner ( + inner, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume inner ) + } + stmt.ConversationHandles = append(stmt.ConversationHandles, &ast.ParenthesisExpression{Expression: inner}) + if p.curTok.Type == TokenRParen { + p.nextToken() // consume outer ) + } + } else { + // Parse comma-separated list of expressions + for { + handle, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.ConversationHandles = append(stmt.ConversationHandles, handle) + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + continue + } + break + } + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + } + } else { + // Non-parenthesized expression handle, err := p.parseScalarExpression() if err != nil { return nil, err } stmt.ConversationHandles = append(stmt.ConversationHandles, handle) - - if p.curTok.Type != TokenComma { - break - } - p.nextToken() // consume comma } // Optional MESSAGE TYPE @@ -5734,6 +5776,12 @@ func (p *Parser) parseWorkloadGroupParameter() (interface{}, error) { switch importanceValue { case "LOW": importanceValue = "Low" + case "BELOW_NORMAL": + importanceValue = "Below_Normal" + case "NORMAL": + importanceValue = "Normal" + case "ABOVE_NORMAL": + importanceValue = "Above_Normal" case "MEDIUM": importanceValue = "Medium" case "HIGH": @@ -5766,6 +5814,10 @@ func (p *Parser) parseWorkloadGroupParameter() (interface{}, error) { param.ParameterType = "MinPercentageResource" case "QUERY_EXECUTION_TIMEOUT_SEC": param.ParameterType = "QueryExecutionTimeoutSec" + case "REQUEST_MIN_RESOURCE_GRANT_PERCENT": + param.ParameterType = "RequestMinResourceGrantPercent" + case "REQUEST_MAX_RESOURCE_GRANT_PERCENT": + param.ParameterType = "RequestMaxResourceGrantPercent" default: param.ParameterType = paramName } @@ -5780,6 +5832,170 @@ func (p *Parser) parseWorkloadGroupParameter() (interface{}, error) { return param, nil } +// parseCreateWorkloadClassifierStatement parses CREATE WORKLOAD CLASSIFIER statement. +func (p *Parser) parseCreateWorkloadClassifierStatement() (*ast.CreateWorkloadClassifierStatement, error) { + // Consume WORKLOAD + p.nextToken() + + // Consume CLASSIFIER + if strings.ToUpper(p.curTok.Literal) == "CLASSIFIER" { + p.nextToken() + } + + stmt := &ast.CreateWorkloadClassifierStatement{} + + // Parse classifier name + stmt.ClassifierName = p.parseIdentifier() + + // Parse WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + } + + // Parse options + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + opt, err := p.parseWorkloadClassifierOption() + if err != nil { + return nil, err + } + if opt != nil { + stmt.Options = append(stmt.Options, opt) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +// parseWorkloadClassifierOption parses a single workload classifier option. +func (p *Parser) parseWorkloadClassifierOption() (ast.WorkloadClassifierOption, error) { + // Parse option name + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + // Parse = value + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + switch optName { + case "WORKLOAD_GROUP": + opt := &ast.ClassifierWorkloadGroupOption{ + OptionType: "WorkloadGroup", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.WorkloadGroupName = strLit + return opt, nil + + case "MEMBERNAME": + opt := &ast.ClassifierMemberNameOption{ + OptionType: "MemberName", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.MemberName = strLit + return opt, nil + + case "WLM_CONTEXT": + opt := &ast.ClassifierWlmContextOption{ + OptionType: "WlmContext", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.WlmContext = strLit + return opt, nil + + case "START_TIME": + opt := &ast.ClassifierStartTimeOption{ + OptionType: "StartTime", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.Time = &ast.WlmTimeLiteral{ + TimeString: strLit, + } + return opt, nil + + case "END_TIME": + opt := &ast.ClassifierEndTimeOption{ + OptionType: "EndTime", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.Time = &ast.WlmTimeLiteral{ + TimeString: strLit, + } + return opt, nil + + case "WLM_LABEL": + opt := &ast.ClassifierWlmLabelOption{ + OptionType: "WlmLabel", + } + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + opt.WlmLabel = strLit + return opt, nil + + case "IMPORTANCE": + opt := &ast.ClassifierImportanceOption{ + OptionType: "Importance", + } + importanceValue := strings.ToUpper(p.curTok.Literal) + switch importanceValue { + case "LOW": + opt.Importance = "Low" + case "BELOW_NORMAL": + opt.Importance = "Below_Normal" + case "NORMAL": + opt.Importance = "Normal" + case "ABOVE_NORMAL": + opt.Importance = "Above_Normal" + case "HIGH": + opt.Importance = "High" + default: + opt.Importance = importanceValue + } + p.nextToken() + return opt, nil + + default: + // Skip unknown option + if p.curTok.Type != TokenComma && p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + p.nextToken() + } + return nil, nil + } +} + // parseDbccStatement parses a DBCC statement. func (p *Parser) parseDbccStatement() (*ast.DbccStatement, error) { // Consume DBCC diff --git a/parser/testdata/AlterWorkloadGroupStatementSqlDwTests/metadata.json b/parser/testdata/AlterWorkloadGroupStatementSqlDwTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/AlterWorkloadGroupStatementSqlDwTests/metadata.json +++ b/parser/testdata/AlterWorkloadGroupStatementSqlDwTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/AssignmentWithOperationTests/metadata.json b/parser/testdata/AssignmentWithOperationTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/AssignmentWithOperationTests/metadata.json +++ b/parser/testdata/AssignmentWithOperationTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines100_AssignmentWithOperationTests/metadata.json b/parser/testdata/Baselines100_AssignmentWithOperationTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines100_AssignmentWithOperationTests/metadata.json +++ b/parser/testdata/Baselines100_AssignmentWithOperationTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines110_SendStatementTests/metadata.json b/parser/testdata/Baselines110_SendStatementTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines110_SendStatementTests/metadata.json +++ b/parser/testdata/Baselines110_SendStatementTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines110_SendStatementTests110/metadata.json b/parser/testdata/Baselines110_SendStatementTests110/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines110_SendStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_SendStatementTests110/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines130_AlterWorkloadGroupStatementSqlDwTests/metadata.json b/parser/testdata/Baselines130_AlterWorkloadGroupStatementSqlDwTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines130_AlterWorkloadGroupStatementSqlDwTests/metadata.json +++ b/parser/testdata/Baselines130_AlterWorkloadGroupStatementSqlDwTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines130_CreateWorkloadClassifierStatementSqlDwTests/metadata.json b/parser/testdata/Baselines130_CreateWorkloadClassifierStatementSqlDwTests/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines130_CreateWorkloadClassifierStatementSqlDwTests/metadata.json +++ b/parser/testdata/Baselines130_CreateWorkloadClassifierStatementSqlDwTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines130_CreateWorkloadGroupStatementSqlDwTests130/metadata.json b/parser/testdata/Baselines130_CreateWorkloadGroupStatementSqlDwTests130/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines130_CreateWorkloadGroupStatementSqlDwTests130/metadata.json +++ b/parser/testdata/Baselines130_CreateWorkloadGroupStatementSqlDwTests130/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines160_FuzzyStringMatchingTests160/metadata.json b/parser/testdata/Baselines160_FuzzyStringMatchingTests160/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines160_FuzzyStringMatchingTests160/metadata.json +++ b/parser/testdata/Baselines160_FuzzyStringMatchingTests160/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/Baselines160_StringConcatOperatorTests160/metadata.json b/parser/testdata/Baselines160_StringConcatOperatorTests160/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/Baselines160_StringConcatOperatorTests160/metadata.json +++ b/parser/testdata/Baselines160_StringConcatOperatorTests160/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/CreateWorkloadClassifierStatementSqlDwTests/metadata.json b/parser/testdata/CreateWorkloadClassifierStatementSqlDwTests/metadata.json index 92f70877..4cf76430 100644 --- a/parser/testdata/CreateWorkloadClassifierStatementSqlDwTests/metadata.json +++ b/parser/testdata/CreateWorkloadClassifierStatementSqlDwTests/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} \ No newline at end of file diff --git a/parser/testdata/CreateWorkloadGroupStatementSqlDwTests130/metadata.json b/parser/testdata/CreateWorkloadGroupStatementSqlDwTests130/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/CreateWorkloadGroupStatementSqlDwTests130/metadata.json +++ b/parser/testdata/CreateWorkloadGroupStatementSqlDwTests130/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/FuzzyStringMatchingTests160/metadata.json b/parser/testdata/FuzzyStringMatchingTests160/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/FuzzyStringMatchingTests160/metadata.json +++ b/parser/testdata/FuzzyStringMatchingTests160/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/SendStatementTests110/metadata.json b/parser/testdata/SendStatementTests110/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/SendStatementTests110/metadata.json +++ b/parser/testdata/SendStatementTests110/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/parser/testdata/StringConcatOperatorTests160/metadata.json b/parser/testdata/StringConcatOperatorTests160/metadata.json index 92f70877..e27d63a6 100644 --- a/parser/testdata/StringConcatOperatorTests160/metadata.json +++ b/parser/testdata/StringConcatOperatorTests160/metadata.json @@ -1 +1 @@ -{"skip": true} \ No newline at end of file +{"skip": false} diff --git a/skipped_tests_by_size.txt b/skipped_tests_by_size.txt index 5b12841a..1715ad00 100644 --- a/skipped_tests_by_size.txt +++ b/skipped_tests_by_size.txt @@ -26,9 +26,7 @@ 236 AlterTableStatementTests140_Azure 236 Baselines140_AlterTableStatementTests140_Azure 239 Baselines100_EventNotificationStatementTests100 -239 Baselines160_FuzzyStringMatchingTests160 240 RowsetsInSelectTests90 -243 CreateWorkloadClassifierStatementSqlDwTests 245 BaselinesCommon_SetVariableStatementTests 247 VectorTypeSecondParameterTests170 250 CreateSpatialIndexStatementTests110 @@ -44,7 +42,6 @@ 278 BaselinesFabricDW_CloneTableTestsFabricDW 278 CloneTableTestsFabricDW 280 WhitespaceTests -286 Baselines110_SendStatementTests 286 Baselines130_SecurityStatement130Tests 286 SecurityStatement130Tests 288 BaselinesCommon_TSqlParserTestScript3 @@ -59,14 +56,11 @@ 304 Baselines90_DumpLoadStatement90Tests 306 CreateTypeStatementTests 308 EventNotificationStatementTests100 -308 SendStatementTests110 308 TSqlParserTestScript2 309 Baselines110_CreateSpatialIndexStatementTests110 311 Baselines130_CreateHekatonTriggerStatementTest -312 Baselines110_SendStatementTests110 313 Baselines160_CreateFunctionStatementTests160 315 ServerAuditStatementTests110 -317 Baselines130_CreateWorkloadClassifierStatementSqlDwTests 320 BaselinesCommon_AlterFunctionStatementTests 322 Baselines90_CreateFulltextCatalogStatementTests 324 BaselinesCommon_DeleteStatementTests @@ -77,13 +71,11 @@ 332 SetVariableStatementTests90 334 AlterExternalLibrary140 334 Baselines140_AlterExternalLibrary140 -334 FuzzyStringMatchingTests160 337 Baselines120_FromClauseTests120 338 CreateAlterDatabaseStatementTestsAzure110 339 AsymmetricKeyStatementTests100 340 Baselines140_CreateExternalLibrary140 340 CreateExternalLibrary140 -340 StringConcatOperatorTests160 341 Baselines100_TableParametersTests 344 Baselines110_ServerAuditStatementTests110 345 CreateSequenceStatementTests @@ -98,7 +90,6 @@ 372 Baselines110_CreateAlterDatabaseStatementTestsAzure110 376 BaselinesCommon_SetOffsetsAndOnOffSetTests 377 Baselines140_CreateDatabaseTests140 -378 Baselines160_StringConcatOperatorTests160 379 AlterCreateServiceStatementTests 382 Baselines90_AlterCreateServiceStatementTests 382 TableParametersTests @@ -351,7 +342,6 @@ 824 AlterTableAlterColumnStatementTests 828 AlterFulltextIndexStatementTests 832 FromClauseTests110 -841 Baselines100_AssignmentWithOperationTests 858 SetCommandsAndMiscTests 863 CreateAlterResourcePoolStatementTests110 867 BaselinesCommon_CreateDatabaseStatementTests @@ -360,7 +350,6 @@ 881 Baselines80_SelectStatementDeprecatedIn110Tests 881 Baselines90_CTEStatementTests 889 LoginStatementTests150 -892 AssignmentWithOperationTests 892 CreateAlterDropFulltextStoplistStatementTests 895 CreateTableTests90 898 CTEStatementTests @@ -490,7 +479,6 @@ 1416 BaselinesCommon_FunctionStatementTests 1428 JsonIndexTests170 1431 FunctionStatementTests -1445 AlterWorkloadGroupStatementSqlDwTests 1470 LoginStatementTests 1472 OpenRowsetBulkStatementTests160 1475 Baselines90_DropStatementsTests2 @@ -558,7 +546,6 @@ 1929 AlterIndexStatementTests120 1931 Baselines120_AlterTableStatementTests120 1934 AlterIndexStatementTests140 -1939 Baselines130_AlterWorkloadGroupStatementSqlDwTests 1954 Baselines130_CtasStatementTests 1955 Baselines160_BuiltInFunctionTests160 1966 BuiltInFunctionTests160 @@ -585,9 +572,7 @@ 2180 FromClauseTests90 2189 Baselines90_FulltextIndexStatementTests 2190 Baselines130_TemporalSelectTest130 -2264 CreateWorkloadGroupStatementSqlDwTests130 2294 Baselines150_OpenRowsetBulkStatementTests150 -2327 Baselines130_CreateWorkloadGroupStatementSqlDwTests130 2343 RowsetsInSelectTests 2345 TemporalSelectTest130 2366 BaselinesCommon_CreateProcedureStatementTests