From 94bc7c061faa5f27e6eb8eda712f886ad2aa2dad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 15:55:36 +0000 Subject: [PATCH] Add full CREATE EVENT NOTIFICATION parsing support Parse complete CREATE EVENT NOTIFICATION syntax including: - ON SERVER/DATABASE/QUEUE scope with queue name support - WITH FAN_IN option - Event type and event group lists with PascalCase conversion - TO SERVICE with broker service and instance specifier Enable Baselines100_EventNotificationStatementTests100 and EventNotificationStatementTests100 tests. --- ast/create_simple_statements.go | 29 ++++- ast/create_trigger_statement.go | 5 +- parser/marshal.go | 46 +++++++ parser/parse_statements.go | 122 +++++++++++++++++- .../metadata.json | 2 +- .../metadata.json | 2 +- 6 files changed, 201 insertions(+), 5 deletions(-) diff --git a/ast/create_simple_statements.go b/ast/create_simple_statements.go index 377722da..056b59c0 100644 --- a/ast/create_simple_statements.go +++ b/ast/create_simple_statements.go @@ -215,9 +215,36 @@ type CreateXmlIndexStatement struct { func (s *CreateXmlIndexStatement) node() {} func (s *CreateXmlIndexStatement) statement() {} +// EventNotificationObjectScope represents the scope of an event notification (SERVER, DATABASE, or QUEUE). +type EventNotificationObjectScope struct { + Target string `json:"Target,omitempty"` // "Server", "Database", or "Queue" + QueueName *SchemaObjectName `json:"QueueName,omitempty"` +} + +func (s *EventNotificationObjectScope) node() {} + +// EventTypeGroupContainer is an interface for event type/group containers. +type EventTypeGroupContainer interface { + node() + eventTypeGroupContainer() +} + +// EventGroupContainer represents a group of events. +type EventGroupContainer struct { + EventGroup string `json:"EventGroup,omitempty"` +} + +func (c *EventGroupContainer) node() {} +func (c *EventGroupContainer) eventTypeGroupContainer() {} + // CreateEventNotificationStatement represents a CREATE EVENT NOTIFICATION statement. type CreateEventNotificationStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + Scope *EventNotificationObjectScope `json:"Scope,omitempty"` + WithFanIn bool `json:"WithFanIn,omitempty"` + EventTypeGroups []EventTypeGroupContainer `json:"EventTypeGroups,omitempty"` + BrokerService *StringLiteral `json:"BrokerService,omitempty"` + BrokerInstanceSpecifier *StringLiteral `json:"BrokerInstanceSpecifier,omitempty"` } func (s *CreateEventNotificationStatement) node() {} diff --git a/ast/create_trigger_statement.go b/ast/create_trigger_statement.go index 29fb325f..115ba765 100644 --- a/ast/create_trigger_statement.go +++ b/ast/create_trigger_statement.go @@ -18,5 +18,8 @@ func (s *CreateTriggerStatement) node() {} // EventTypeContainer represents an event type container type EventTypeContainer struct { - EventType string + EventType string `json:"EventType,omitempty"` } + +func (c *EventTypeContainer) node() {} +func (c *EventTypeContainer) eventTypeGroupContainer() {} diff --git a/parser/marshal.go b/parser/marshal.go index b0773f93..70850d0a 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -7005,9 +7005,55 @@ func createEventNotificationStatementToJSON(s *ast.CreateEventNotificationStatem if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.Scope != nil { + node["Scope"] = eventNotificationObjectScopeToJSON(s.Scope) + // Include WithFanIn when Scope is present + node["WithFanIn"] = s.WithFanIn + } + if len(s.EventTypeGroups) > 0 { + groups := make([]jsonNode, len(s.EventTypeGroups)) + for i, g := range s.EventTypeGroups { + groups[i] = eventTypeGroupContainerToJSON(g) + } + node["EventTypeGroups"] = groups + } + if s.BrokerService != nil { + node["BrokerService"] = stringLiteralToJSON(s.BrokerService) + } + if s.BrokerInstanceSpecifier != nil { + node["BrokerInstanceSpecifier"] = stringLiteralToJSON(s.BrokerInstanceSpecifier) + } + return node +} + +func eventNotificationObjectScopeToJSON(s *ast.EventNotificationObjectScope) jsonNode { + node := jsonNode{ + "$type": "EventNotificationObjectScope", + "Target": s.Target, + } + if s.QueueName != nil { + node["QueueName"] = schemaObjectNameToJSON(s.QueueName) + } return node } +func eventTypeGroupContainerToJSON(c ast.EventTypeGroupContainer) jsonNode { + switch v := c.(type) { + case *ast.EventTypeContainer: + return jsonNode{ + "$type": "EventTypeContainer", + "EventType": v.EventType, + } + case *ast.EventGroupContainer: + return jsonNode{ + "$type": "EventGroupContainer", + "EventGroup": v.EventGroup, + } + default: + return jsonNode{"$type": "Unknown"} + } +} + func alterDatabaseAddFileStatementToJSON(s *ast.AlterDatabaseAddFileStatement) jsonNode { node := jsonNode{ "$type": "AlterDatabaseAddFileStatement", diff --git a/parser/parse_statements.go b/parser/parse_statements.go index ba5310a6..d16a68e5 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -4525,11 +4525,131 @@ func (p *Parser) parseCreateEventNotificationFromEvent() (*ast.CreateEventNotifi Name: p.parseIdentifier(), } - // Skip rest of statement + // Parse ON + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + stmt.Scope = &ast.EventNotificationObjectScope{} + + scopeUpper := strings.ToUpper(p.curTok.Literal) + switch scopeUpper { + case "SERVER": + stmt.Scope.Target = "Server" + p.nextToken() + case "DATABASE": + stmt.Scope.Target = "Database" + p.nextToken() + case "QUEUE": + stmt.Scope.Target = "Queue" + p.nextToken() + // Parse queue name + stmt.Scope.QueueName, _ = p.parseSchemaObjectName() + } + } + + // Parse optional WITH FAN_IN + if strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + if strings.ToUpper(p.curTok.Literal) == "FAN_IN" { + stmt.WithFanIn = true + p.nextToken() // consume FAN_IN + } + } + + // Parse FOR + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + + // Parse comma-separated list of event types/groups + for { + eventName := p.curTok.Literal + p.nextToken() + + // Convert event name to PascalCase and determine if it's a group or type + pascalName := eventNameToPascalCase(eventName) + + // If name ends with "Events" (after conversion), it's a group + if strings.HasSuffix(strings.ToUpper(eventName), "_EVENTS") || strings.HasSuffix(strings.ToUpper(eventName), "EVENTS") { + stmt.EventTypeGroups = append(stmt.EventTypeGroups, &ast.EventGroupContainer{ + EventGroup: pascalName, + }) + } else { + stmt.EventTypeGroups = append(stmt.EventTypeGroups, &ast.EventTypeContainer{ + EventType: pascalName, + }) + } + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + } + + // Parse TO SERVICE 'service_name', 'broker_instance' + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + if strings.ToUpper(p.curTok.Literal) == "SERVICE" { + p.nextToken() // consume SERVICE + + // Parse broker service name (string literal) + if p.curTok.Type == TokenString { + litVal := p.curTok.Literal + // Strip surrounding quotes + if len(litVal) >= 2 && litVal[0] == '\'' && litVal[len(litVal)-1] == '\'' { + litVal = litVal[1 : len(litVal)-1] + } + stmt.BrokerService = &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: litVal, + } + p.nextToken() + } + + // Parse comma and broker instance specifier + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + + if p.curTok.Type == TokenString { + litVal := p.curTok.Literal + // Strip surrounding quotes + if len(litVal) >= 2 && litVal[0] == '\'' && litVal[len(litVal)-1] == '\'' { + litVal = litVal[1 : len(litVal)-1] + } + stmt.BrokerInstanceSpecifier = &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: litVal, + } + p.nextToken() + } + } + } + } + + // Skip any remaining tokens p.skipToEndOfStatement() return stmt, nil } +// eventNameToPascalCase converts an event name like "Object_Created" or "DDL_CREDENTIAL_EVENTS" to PascalCase. +func eventNameToPascalCase(name string) string { + // Split by underscore + parts := strings.Split(name, "_") + var result strings.Builder + for _, part := range parts { + if len(part) == 0 { + continue + } + // Capitalize first letter, lowercase rest + result.WriteString(strings.ToUpper(part[:1])) + result.WriteString(strings.ToLower(part[1:])) + } + return result.String() +} + func (p *Parser) parseCreatePartitionFunctionFromPartition() (*ast.CreatePartitionFunctionStatement, error) { // PARTITION has already been consumed, curTok is FUNCTION if strings.ToUpper(p.curTok.Literal) == "FUNCTION" { diff --git a/parser/testdata/Baselines100_EventNotificationStatementTests100/metadata.json b/parser/testdata/Baselines100_EventNotificationStatementTests100/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/Baselines100_EventNotificationStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_EventNotificationStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/EventNotificationStatementTests100/metadata.json b/parser/testdata/EventNotificationStatementTests100/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/EventNotificationStatementTests100/metadata.json +++ b/parser/testdata/EventNotificationStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file