Skip to content

Commit 74b4588

Browse files
committed
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
1 parent 8939ee0 commit 74b4588

6 files changed

Lines changed: 337 additions & 2 deletions

File tree

ast/begin_dialog_statement.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ast
2+
3+
// BeginDialogStatement represents a BEGIN DIALOG statement for SQL Server Service Broker.
4+
type BeginDialogStatement struct {
5+
IsConversation bool `json:"IsConversation,omitempty"`
6+
Handle ScalarExpression `json:"Handle,omitempty"`
7+
InitiatorServiceName *IdentifierOrValueExpression `json:"InitiatorServiceName,omitempty"`
8+
TargetServiceName ScalarExpression `json:"TargetServiceName,omitempty"`
9+
ContractName *IdentifierOrValueExpression `json:"ContractName,omitempty"`
10+
InstanceSpec ScalarExpression `json:"InstanceSpec,omitempty"`
11+
Options []DialogOption `json:"Options,omitempty"`
12+
}
13+
14+
func (s *BeginDialogStatement) node() {}
15+
func (s *BeginDialogStatement) statement() {}
16+
17+
// BeginConversationTimerStatement represents a BEGIN CONVERSATION TIMER statement.
18+
type BeginConversationTimerStatement struct {
19+
Handle ScalarExpression `json:"Handle,omitempty"`
20+
Timeout ScalarExpression `json:"Timeout,omitempty"`
21+
}
22+
23+
func (s *BeginConversationTimerStatement) node() {}
24+
func (s *BeginConversationTimerStatement) statement() {}
25+
26+
// DialogOption is an interface for dialog options.
27+
type DialogOption interface {
28+
dialogOption()
29+
}
30+
31+
// ScalarExpressionDialogOption represents a dialog option with a scalar expression value.
32+
type ScalarExpressionDialogOption struct {
33+
Value ScalarExpression `json:"Value,omitempty"`
34+
OptionKind string `json:"OptionKind,omitempty"` // RelatedConversation, RelatedConversationGroup, Lifetime
35+
}
36+
37+
func (o *ScalarExpressionDialogOption) dialogOption() {}
38+
39+
// OnOffDialogOption represents a dialog option with an ON/OFF value.
40+
type OnOffDialogOption struct {
41+
OptionState string `json:"OptionState,omitempty"` // On, Off
42+
OptionKind string `json:"OptionKind,omitempty"` // Encryption
43+
}
44+
45+
func (o *OnOffDialogOption) dialogOption() {}

parser/lexer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ const (
185185
TokenColonColon
186186
TokenMove
187187
TokenConversation
188+
TokenDialog
188189
TokenGet
189190
TokenUse
190191
TokenKill
@@ -925,6 +926,7 @@ var keywords = map[string]TokenType{
925926
"TRUNCATE": TokenTruncate,
926927
"MOVE": TokenMove,
927928
"CONVERSATION": TokenConversation,
929+
"DIALOG": TokenDialog,
928930
"GET": TokenGet,
929931
"USE": TokenUse,
930932
"KILL": TokenKill,

parser/marshal.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ func statementToJSON(stmt ast.Statement) jsonNode {
7272
return beginEndBlockStatementToJSON(s)
7373
case *ast.BeginEndAtomicBlockStatement:
7474
return beginEndAtomicBlockStatementToJSON(s)
75+
case *ast.BeginDialogStatement:
76+
return beginDialogStatementToJSON(s)
77+
case *ast.BeginConversationTimerStatement:
78+
return beginConversationTimerStatementToJSON(s)
7579
case *ast.CreateViewStatement:
7680
return createViewStatementToJSON(s)
7781
case *ast.CreateSchemaStatement:
@@ -2861,6 +2865,71 @@ func statementListToJSON(sl *ast.StatementList) jsonNode {
28612865
return node
28622866
}
28632867

2868+
func beginDialogStatementToJSON(s *ast.BeginDialogStatement) jsonNode {
2869+
node := jsonNode{
2870+
"$type": "BeginDialogStatement",
2871+
"IsConversation": s.IsConversation,
2872+
}
2873+
if s.Handle != nil {
2874+
node["Handle"] = scalarExpressionToJSON(s.Handle)
2875+
}
2876+
if s.InitiatorServiceName != nil {
2877+
node["InitiatorServiceName"] = identifierOrValueExpressionToJSON(s.InitiatorServiceName)
2878+
}
2879+
if s.TargetServiceName != nil {
2880+
node["TargetServiceName"] = scalarExpressionToJSON(s.TargetServiceName)
2881+
}
2882+
if s.ContractName != nil {
2883+
node["ContractName"] = identifierOrValueExpressionToJSON(s.ContractName)
2884+
}
2885+
if s.InstanceSpec != nil {
2886+
node["InstanceSpec"] = scalarExpressionToJSON(s.InstanceSpec)
2887+
}
2888+
if len(s.Options) > 0 {
2889+
options := make([]jsonNode, len(s.Options))
2890+
for i, o := range s.Options {
2891+
options[i] = dialogOptionToJSON(o)
2892+
}
2893+
node["Options"] = options
2894+
}
2895+
return node
2896+
}
2897+
2898+
func dialogOptionToJSON(o ast.DialogOption) jsonNode {
2899+
switch opt := o.(type) {
2900+
case *ast.ScalarExpressionDialogOption:
2901+
node := jsonNode{
2902+
"$type": "ScalarExpressionDialogOption",
2903+
"OptionKind": opt.OptionKind,
2904+
}
2905+
if opt.Value != nil {
2906+
node["Value"] = scalarExpressionToJSON(opt.Value)
2907+
}
2908+
return node
2909+
case *ast.OnOffDialogOption:
2910+
return jsonNode{
2911+
"$type": "OnOffDialogOption",
2912+
"OptionState": opt.OptionState,
2913+
"OptionKind": opt.OptionKind,
2914+
}
2915+
default:
2916+
return jsonNode{"$type": "UnknownDialogOption"}
2917+
}
2918+
}
2919+
2920+
func beginConversationTimerStatementToJSON(s *ast.BeginConversationTimerStatement) jsonNode {
2921+
node := jsonNode{
2922+
"$type": "BeginConversationTimerStatement",
2923+
}
2924+
if s.Handle != nil {
2925+
node["Handle"] = scalarExpressionToJSON(s.Handle)
2926+
}
2927+
if s.Timeout != nil {
2928+
node["Timeout"] = scalarExpressionToJSON(s.Timeout)
2929+
}
2930+
return node
2931+
}
2932+
28642933
func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode {
28652934
node := jsonNode{
28662935
"$type": "CreateViewStatement",

parser/parse_statements.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,10 @@ func (p *Parser) parseBeginStatement() (ast.Statement, error) {
13181318
return p.parseBeginTransactionStatementContinued(false)
13191319
case TokenTry:
13201320
return p.parseTryCatchStatement()
1321+
case TokenDialog:
1322+
return p.parseBeginDialogStatement()
1323+
case TokenConversation:
1324+
return p.parseBeginConversationTimerStatement()
13211325
case TokenIdent:
13221326
// Check for DISTRIBUTED
13231327
if strings.ToUpper(p.curTok.Literal) == "DISTRIBUTED" {
@@ -1660,6 +1664,221 @@ func (p *Parser) parseBeginEndBlockStatementContinued() (*ast.BeginEndBlockState
16601664
return stmt, nil
16611665
}
16621666

1667+
func (p *Parser) parseBeginDialogStatement() (*ast.BeginDialogStatement, error) {
1668+
p.nextToken() // consume DIALOG
1669+
1670+
stmt := &ast.BeginDialogStatement{}
1671+
1672+
// Check for optional CONVERSATION keyword
1673+
if p.curTok.Type == TokenConversation {
1674+
stmt.IsConversation = true
1675+
p.nextToken() // consume CONVERSATION
1676+
}
1677+
1678+
// Parse dialog handle (variable reference)
1679+
if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1680+
stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal}
1681+
p.nextToken()
1682+
} else {
1683+
return nil, fmt.Errorf("expected variable for dialog handle")
1684+
}
1685+
1686+
// Parse FROM SERVICE
1687+
if p.curTok.Type != TokenFrom {
1688+
return nil, fmt.Errorf("expected FROM after dialog handle")
1689+
}
1690+
p.nextToken() // consume FROM
1691+
1692+
if strings.ToUpper(p.curTok.Literal) != "SERVICE" {
1693+
return nil, fmt.Errorf("expected SERVICE after FROM")
1694+
}
1695+
p.nextToken() // consume SERVICE
1696+
1697+
// Parse initiator service name (identifier)
1698+
id := p.parseIdentifier()
1699+
stmt.InitiatorServiceName = &ast.IdentifierOrValueExpression{
1700+
Value: id.Value,
1701+
Identifier: id,
1702+
}
1703+
1704+
// Parse TO SERVICE
1705+
if p.curTok.Type != TokenTo {
1706+
return nil, fmt.Errorf("expected TO after initiator service name")
1707+
}
1708+
p.nextToken() // consume TO
1709+
1710+
if strings.ToUpper(p.curTok.Literal) != "SERVICE" {
1711+
return nil, fmt.Errorf("expected SERVICE after TO")
1712+
}
1713+
p.nextToken() // consume SERVICE
1714+
1715+
// Parse target service name (string literal or variable)
1716+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
1717+
strLit, err := p.parseStringLiteral()
1718+
if err != nil {
1719+
return nil, err
1720+
}
1721+
stmt.TargetServiceName = strLit
1722+
} else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1723+
stmt.TargetServiceName = &ast.VariableReference{Name: p.curTok.Literal}
1724+
p.nextToken()
1725+
} else {
1726+
return nil, fmt.Errorf("expected string literal or variable for target service name")
1727+
}
1728+
1729+
// Check for optional instance spec (after comma)
1730+
if p.curTok.Type == TokenComma {
1731+
p.nextToken() // consume comma
1732+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
1733+
strLit, err := p.parseStringLiteral()
1734+
if err != nil {
1735+
return nil, err
1736+
}
1737+
stmt.InstanceSpec = strLit
1738+
} else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1739+
stmt.InstanceSpec = &ast.VariableReference{Name: p.curTok.Literal}
1740+
p.nextToken()
1741+
}
1742+
}
1743+
1744+
// Parse optional ON CONTRACT
1745+
if p.curTok.Type == TokenOn && strings.ToUpper(p.peekTok.Literal) == "CONTRACT" {
1746+
p.nextToken() // consume ON
1747+
p.nextToken() // consume CONTRACT
1748+
id := p.parseIdentifier()
1749+
stmt.ContractName = &ast.IdentifierOrValueExpression{
1750+
Value: id.Value,
1751+
Identifier: id,
1752+
}
1753+
}
1754+
1755+
// Parse optional WITH options
1756+
if p.curTok.Type == TokenWith {
1757+
p.nextToken() // consume WITH
1758+
for {
1759+
optName := strings.ToUpper(p.curTok.Literal)
1760+
p.nextToken() // consume option name
1761+
1762+
if p.curTok.Type == TokenEquals {
1763+
p.nextToken() // consume =
1764+
}
1765+
1766+
switch optName {
1767+
case "RELATED_CONVERSATION":
1768+
if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1769+
stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{
1770+
Value: &ast.VariableReference{Name: p.curTok.Literal},
1771+
OptionKind: "RelatedConversation",
1772+
})
1773+
p.nextToken()
1774+
}
1775+
case "RELATED_CONVERSATION_GROUP":
1776+
if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1777+
stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{
1778+
Value: &ast.VariableReference{Name: p.curTok.Literal},
1779+
OptionKind: "RelatedConversationGroup",
1780+
})
1781+
p.nextToken()
1782+
}
1783+
case "ENCRYPTION":
1784+
optState := strings.ToUpper(p.curTok.Literal)
1785+
if optState == "ON" {
1786+
stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{
1787+
OptionState: "On",
1788+
OptionKind: "Encryption",
1789+
})
1790+
} else if optState == "OFF" {
1791+
stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{
1792+
OptionState: "Off",
1793+
OptionKind: "Encryption",
1794+
})
1795+
}
1796+
p.nextToken()
1797+
case "LIFETIME":
1798+
if p.curTok.Type == TokenNumber {
1799+
stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{
1800+
Value: &ast.IntegerLiteral{
1801+
LiteralType: "Integer",
1802+
Value: p.curTok.Literal,
1803+
},
1804+
OptionKind: "Lifetime",
1805+
})
1806+
p.nextToken()
1807+
}
1808+
}
1809+
1810+
if p.curTok.Type != TokenComma {
1811+
break
1812+
}
1813+
p.nextToken() // consume comma
1814+
}
1815+
}
1816+
1817+
// Skip optional semicolon
1818+
if p.curTok.Type == TokenSemicolon {
1819+
p.nextToken()
1820+
}
1821+
1822+
return stmt, nil
1823+
}
1824+
1825+
func (p *Parser) parseBeginConversationTimerStatement() (*ast.BeginConversationTimerStatement, error) {
1826+
p.nextToken() // consume CONVERSATION
1827+
1828+
// Expect TIMER
1829+
if strings.ToUpper(p.curTok.Literal) != "TIMER" {
1830+
return nil, fmt.Errorf("expected TIMER after BEGIN CONVERSATION")
1831+
}
1832+
p.nextToken() // consume TIMER
1833+
1834+
stmt := &ast.BeginConversationTimerStatement{}
1835+
1836+
// Parse handle in parentheses
1837+
if p.curTok.Type != TokenLParen {
1838+
return nil, fmt.Errorf("expected ( after TIMER")
1839+
}
1840+
p.nextToken() // consume (
1841+
1842+
if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' {
1843+
stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal}
1844+
p.nextToken()
1845+
} else {
1846+
return nil, fmt.Errorf("expected variable for conversation handle")
1847+
}
1848+
1849+
if p.curTok.Type != TokenRParen {
1850+
return nil, fmt.Errorf("expected ) after handle")
1851+
}
1852+
p.nextToken() // consume )
1853+
1854+
// Parse TIMEOUT = value
1855+
if strings.ToUpper(p.curTok.Literal) != "TIMEOUT" {
1856+
return nil, fmt.Errorf("expected TIMEOUT")
1857+
}
1858+
p.nextToken() // consume TIMEOUT
1859+
1860+
if p.curTok.Type == TokenEquals {
1861+
p.nextToken() // consume =
1862+
}
1863+
1864+
if p.curTok.Type == TokenNumber {
1865+
stmt.Timeout = &ast.IntegerLiteral{
1866+
LiteralType: "Integer",
1867+
Value: p.curTok.Literal,
1868+
}
1869+
p.nextToken()
1870+
} else {
1871+
return nil, fmt.Errorf("expected integer for timeout value")
1872+
}
1873+
1874+
// Skip optional semicolon
1875+
if p.curTok.Type == TokenSemicolon {
1876+
p.nextToken()
1877+
}
1878+
1879+
return stmt, nil
1880+
}
1881+
16631882
func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, error) {
16641883
// Consume BEGIN
16651884
p.nextToken()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)