diff --git a/doris/ast/ddlnodes.go b/doris/ast/ddlnodes.go index 598e34d4..60d84e85 100644 --- a/doris/ast/ddlnodes.go +++ b/doris/ast/ddlnodes.go @@ -368,3 +368,209 @@ type DropViewStmt struct { func (n *DropViewStmt) Tag() NodeTag { return T_DropViewStmt } var _ Node = (*DropViewStmt)(nil) + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP DDL nodes (T5.4) +// --------------------------------------------------------------------------- + +// CreateWorkloadGroupStmt represents: +// +// CREATE WORKLOAD GROUP [IF NOT EXISTS] name PROPERTIES(...) +type CreateWorkloadGroupStmt struct { + Name string + IfNotExists bool + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *CreateWorkloadGroupStmt) Tag() NodeTag { return T_CreateWorkloadGroupStmt } + +var _ Node = (*CreateWorkloadGroupStmt)(nil) + +// AlterWorkloadGroupStmt represents: +// +// ALTER WORKLOAD GROUP name PROPERTIES(...) +type AlterWorkloadGroupStmt struct { + Name string + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *AlterWorkloadGroupStmt) Tag() NodeTag { return T_AlterWorkloadGroupStmt } + +var _ Node = (*AlterWorkloadGroupStmt)(nil) + +// DropWorkloadGroupStmt represents: +// +// DROP WORKLOAD GROUP [IF EXISTS] name +type DropWorkloadGroupStmt struct { + Name string + IfExists bool + Loc Loc +} + +// Tag implements Node. +func (n *DropWorkloadGroupStmt) Tag() NodeTag { return T_DropWorkloadGroupStmt } + +var _ Node = (*DropWorkloadGroupStmt)(nil) + +// --------------------------------------------------------------------------- +// WORKLOAD POLICY DDL nodes (T5.4) +// --------------------------------------------------------------------------- + +// WorkloadPolicyItem holds a raw-text capture of one condition or action item +// from a WORKLOAD POLICY CONDITIONS/ACTIONS clause. +type WorkloadPolicyItem struct { + RawText string + Loc Loc +} + +// Tag implements Node. +func (n *WorkloadPolicyItem) Tag() NodeTag { return T_WorkloadPolicyItem } + +var _ Node = (*WorkloadPolicyItem)(nil) + +// CreateWorkloadPolicyStmt represents: +// +// CREATE WORKLOAD POLICY [IF NOT EXISTS] name +// CONDITIONS(condition_list) +// ACTIONS(action_list) +// [PROPERTIES(...)] +type CreateWorkloadPolicyStmt struct { + Name string + IfNotExists bool + Conditions []*WorkloadPolicyItem + Actions []*WorkloadPolicyItem + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *CreateWorkloadPolicyStmt) Tag() NodeTag { return T_CreateWorkloadPolicyStmt } + +var _ Node = (*CreateWorkloadPolicyStmt)(nil) + +// AlterWorkloadPolicyStmt represents: +// +// ALTER WORKLOAD POLICY name PROPERTIES(...) +type AlterWorkloadPolicyStmt struct { + Name string + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *AlterWorkloadPolicyStmt) Tag() NodeTag { return T_AlterWorkloadPolicyStmt } + +var _ Node = (*AlterWorkloadPolicyStmt)(nil) + +// DropWorkloadPolicyStmt represents: +// +// DROP WORKLOAD POLICY [IF EXISTS] name +type DropWorkloadPolicyStmt struct { + Name string + IfExists bool + Loc Loc +} + +// Tag implements Node. +func (n *DropWorkloadPolicyStmt) Tag() NodeTag { return T_DropWorkloadPolicyStmt } + +var _ Node = (*DropWorkloadPolicyStmt)(nil) + +// --------------------------------------------------------------------------- +// RESOURCE DDL nodes (T5.4) +// --------------------------------------------------------------------------- + +// CreateResourceStmt represents: +// +// CREATE [EXTERNAL] RESOURCE [IF NOT EXISTS] name PROPERTIES(...) +type CreateResourceStmt struct { + Name string + IfNotExists bool + External bool + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *CreateResourceStmt) Tag() NodeTag { return T_CreateResourceStmt } + +var _ Node = (*CreateResourceStmt)(nil) + +// AlterResourceStmt represents: +// +// ALTER RESOURCE name PROPERTIES(...) +type AlterResourceStmt struct { + Name string + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *AlterResourceStmt) Tag() NodeTag { return T_AlterResourceStmt } + +var _ Node = (*AlterResourceStmt)(nil) + +// DropResourceStmt represents: +// +// DROP RESOURCE [IF EXISTS] name +type DropResourceStmt struct { + Name string + IfExists bool + Loc Loc +} + +// Tag implements Node. +func (n *DropResourceStmt) Tag() NodeTag { return T_DropResourceStmt } + +var _ Node = (*DropResourceStmt)(nil) + +// --------------------------------------------------------------------------- +// SQL BLOCK RULE DDL nodes (T5.4) +// --------------------------------------------------------------------------- + +// CreateSQLBlockRuleStmt represents: +// +// CREATE SQL_BLOCK_RULE [IF NOT EXISTS] name PROPERTIES(...) +type CreateSQLBlockRuleStmt struct { + Name string + IfNotExists bool + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *CreateSQLBlockRuleStmt) Tag() NodeTag { return T_CreateSQLBlockRuleStmt } + +var _ Node = (*CreateSQLBlockRuleStmt)(nil) + +// AlterSQLBlockRuleStmt represents: +// +// ALTER SQL_BLOCK_RULE name PROPERTIES(...) +type AlterSQLBlockRuleStmt struct { + Name string + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *AlterSQLBlockRuleStmt) Tag() NodeTag { return T_AlterSQLBlockRuleStmt } + +var _ Node = (*AlterSQLBlockRuleStmt)(nil) + +// DropSQLBlockRuleStmt represents: +// +// DROP SQL_BLOCK_RULE [IF EXISTS] name +type DropSQLBlockRuleStmt struct { + Name string + IfExists bool + Loc Loc +} + +// Tag implements Node. +func (n *DropSQLBlockRuleStmt) Tag() NodeTag { return T_DropSQLBlockRuleStmt } + +var _ Node = (*DropSQLBlockRuleStmt)(nil) diff --git a/doris/ast/loc.go b/doris/ast/loc.go index cfc31c2d..bbe02ca9 100644 --- a/doris/ast/loc.go +++ b/doris/ast/loc.go @@ -124,6 +124,32 @@ func NodeLoc(n Node) Loc { return v.Loc case *MergeClause: return v.Loc + case *CreateWorkloadGroupStmt: + return v.Loc + case *AlterWorkloadGroupStmt: + return v.Loc + case *DropWorkloadGroupStmt: + return v.Loc + case *WorkloadPolicyItem: + return v.Loc + case *CreateWorkloadPolicyStmt: + return v.Loc + case *AlterWorkloadPolicyStmt: + return v.Loc + case *DropWorkloadPolicyStmt: + return v.Loc + case *CreateResourceStmt: + return v.Loc + case *AlterResourceStmt: + return v.Loc + case *DropResourceStmt: + return v.Loc + case *CreateSQLBlockRuleStmt: + return v.Loc + case *AlterSQLBlockRuleStmt: + return v.Loc + case *DropSQLBlockRuleStmt: + return v.Loc default: return NoLoc() } diff --git a/doris/ast/nodetags.go b/doris/ast/nodetags.go index 1cc7c3e6..877937eb 100644 --- a/doris/ast/nodetags.go +++ b/doris/ast/nodetags.go @@ -206,6 +206,47 @@ const ( // T_MergeClause is the tag for *MergeClause (one WHEN clause inside MERGE). T_MergeClause + + // Workload management DDL nodes (T5.4). + + // T_CreateWorkloadGroupStmt is the tag for *CreateWorkloadGroupStmt. + T_CreateWorkloadGroupStmt + + // T_AlterWorkloadGroupStmt is the tag for *AlterWorkloadGroupStmt. + T_AlterWorkloadGroupStmt + + // T_DropWorkloadGroupStmt is the tag for *DropWorkloadGroupStmt. + T_DropWorkloadGroupStmt + + // T_WorkloadPolicyItem is the tag for *WorkloadPolicyItem. + T_WorkloadPolicyItem + + // T_CreateWorkloadPolicyStmt is the tag for *CreateWorkloadPolicyStmt. + T_CreateWorkloadPolicyStmt + + // T_AlterWorkloadPolicyStmt is the tag for *AlterWorkloadPolicyStmt. + T_AlterWorkloadPolicyStmt + + // T_DropWorkloadPolicyStmt is the tag for *DropWorkloadPolicyStmt. + T_DropWorkloadPolicyStmt + + // T_CreateResourceStmt is the tag for *CreateResourceStmt. + T_CreateResourceStmt + + // T_AlterResourceStmt is the tag for *AlterResourceStmt. + T_AlterResourceStmt + + // T_DropResourceStmt is the tag for *DropResourceStmt. + T_DropResourceStmt + + // T_CreateSQLBlockRuleStmt is the tag for *CreateSQLBlockRuleStmt. + T_CreateSQLBlockRuleStmt + + // T_AlterSQLBlockRuleStmt is the tag for *AlterSQLBlockRuleStmt. + T_AlterSQLBlockRuleStmt + + // T_DropSQLBlockRuleStmt is the tag for *DropSQLBlockRuleStmt. + T_DropSQLBlockRuleStmt ) // String returns a human-readable representation of the tag. @@ -327,6 +368,32 @@ func (t NodeTag) String() string { return "MergeStmt" case T_MergeClause: return "MergeClause" + case T_CreateWorkloadGroupStmt: + return "CreateWorkloadGroupStmt" + case T_AlterWorkloadGroupStmt: + return "AlterWorkloadGroupStmt" + case T_DropWorkloadGroupStmt: + return "DropWorkloadGroupStmt" + case T_WorkloadPolicyItem: + return "WorkloadPolicyItem" + case T_CreateWorkloadPolicyStmt: + return "CreateWorkloadPolicyStmt" + case T_AlterWorkloadPolicyStmt: + return "AlterWorkloadPolicyStmt" + case T_DropWorkloadPolicyStmt: + return "DropWorkloadPolicyStmt" + case T_CreateResourceStmt: + return "CreateResourceStmt" + case T_AlterResourceStmt: + return "AlterResourceStmt" + case T_DropResourceStmt: + return "DropResourceStmt" + case T_CreateSQLBlockRuleStmt: + return "CreateSQLBlockRuleStmt" + case T_AlterSQLBlockRuleStmt: + return "AlterSQLBlockRuleStmt" + case T_DropSQLBlockRuleStmt: + return "DropSQLBlockRuleStmt" default: return "Unknown" } diff --git a/doris/ast/walk_children.go b/doris/ast/walk_children.go index e3b3c3c7..7a8e242e 100644 --- a/doris/ast/walk_children.go +++ b/doris/ast/walk_children.go @@ -348,5 +348,55 @@ func walkChildren(v Visitor, node Node) { Walk(v, val) } } + + // Workload management DDL nodes (T5.4). + case *CreateWorkloadGroupStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *AlterWorkloadGroupStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *DropWorkloadGroupStmt: + // leaf-ish node, no Node children + case *WorkloadPolicyItem: + // leaf node, raw text only + case *CreateWorkloadPolicyStmt: + for _, c := range n.Conditions { + Walk(v, c) + } + for _, a := range n.Actions { + Walk(v, a) + } + for _, prop := range n.Properties { + Walk(v, prop) + } + case *AlterWorkloadPolicyStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *DropWorkloadPolicyStmt: + // leaf-ish node, no Node children + case *CreateResourceStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *AlterResourceStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *DropResourceStmt: + // leaf-ish node, no Node children + case *CreateSQLBlockRuleStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *AlterSQLBlockRuleStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *DropSQLBlockRuleStmt: + // leaf-ish node, no Node children } } diff --git a/doris/parser/parser.go b/doris/parser/parser.go index 1d23e24b..64bccf82 100644 --- a/doris/parser/parser.go +++ b/doris/parser/parser.go @@ -179,8 +179,30 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseCreateIndex(createTok.Loc) case kwDATABASE, kwSCHEMA: return p.parseCreateDatabase() - case kwTABLE, kwEXTERNAL, kwTEMPORARY: + case kwTABLE, kwTEMPORARY: return p.parseCreateTable() + case kwEXTERNAL: + // CREATE EXTERNAL RESOURCE ... or CREATE EXTERNAL TABLE ... + // Peek past EXTERNAL to decide which. + if p.peekNext().Kind == kwRESOURCE { + p.advance() // consume EXTERNAL + return p.parseCreateResource(createTok.Loc, true) + } + return p.parseCreateTable() + case kwWORKLOAD: + // CREATE WORKLOAD GROUP ... or CREATE WORKLOAD POLICY ... + next := p.peekNext() + if next.Kind == kwGROUP { + return p.parseCreateWorkloadGroup(createTok.Loc) + } + if next.Kind == kwPOLICY { + return p.parseCreateWorkloadPolicy(createTok.Loc) + } + return p.unsupported("CREATE WORKLOAD") + case kwRESOURCE: + return p.parseCreateResource(createTok.Loc, false) + case kwSQL_BLOCK_RULE: + return p.parseCreateSQLBlockRule(createTok.Loc) case kwVIEW: return p.parseCreateView(createTok.Loc, false) case kwOR: @@ -202,6 +224,21 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseAlterTable() case kwVIEW: return p.parseAlterView() + case kwWORKLOAD: + // ALTER WORKLOAD GROUP ... or ALTER WORKLOAD POLICY ... + alterLoc := p.prev.Loc + next := p.peekNext() + if next.Kind == kwGROUP { + return p.parseAlterWorkloadGroup(alterLoc) + } + if next.Kind == kwPOLICY { + return p.parseAlterWorkloadPolicy(alterLoc) + } + return p.unsupported("ALTER WORKLOAD") + case kwRESOURCE: + return p.parseAlterResource(p.prev.Loc) + case kwSQL_BLOCK_RULE: + return p.parseAlterSQLBlockRule(p.prev.Loc) default: return p.unsupported("ALTER") } @@ -214,6 +251,20 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseDropDatabase() case kwVIEW: return p.parseDropView(dropTok.Loc) + case kwWORKLOAD: + // DROP WORKLOAD GROUP ... or DROP WORKLOAD POLICY ... + next := p.peekNext() + if next.Kind == kwGROUP { + return p.parseDropWorkloadGroup(dropTok.Loc) + } + if next.Kind == kwPOLICY { + return p.parseDropWorkloadPolicy(dropTok.Loc) + } + return p.unsupported("DROP WORKLOAD") + case kwRESOURCE: + return p.parseDropResource(dropTok.Loc) + case kwSQL_BLOCK_RULE: + return p.parseDropSQLBlockRule(dropTok.Loc) default: return p.unsupported("DROP") } diff --git a/doris/parser/workload.go b/doris/parser/workload.go new file mode 100644 index 00000000..f159daad --- /dev/null +++ b/doris/parser/workload.go @@ -0,0 +1,590 @@ +package parser + +import ( + "strings" + + "github.com/bytebase/omni/doris/ast" +) + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP +// --------------------------------------------------------------------------- + +// parseCreateWorkloadGroup parses: +// +// CREATE WORKLOAD GROUP [IF NOT EXISTS] name PROPERTIES(...) +// +// CREATE has already been consumed; cur is WORKLOAD. The WORKLOAD and GROUP +// tokens are consumed here. +func (p *Parser) parseCreateWorkloadGroup(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwGROUP); err != nil { + return nil, err + } + + stmt := &ast.CreateWorkloadGroupStmt{} + + // Optional IF NOT EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwNOT); err != nil { + return nil, err + } + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfNotExists = true + } + + // Group name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + endLoc := nameLoc + + // Optional PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAlterWorkloadGroup parses: +// +// ALTER WORKLOAD GROUP name PROPERTIES(...) +// +// ALTER has already been consumed; cur is WORKLOAD. The WORKLOAD and GROUP +// tokens are consumed here. +func (p *Parser) parseAlterWorkloadGroup(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwGROUP); err != nil { + return nil, err + } + + stmt := &ast.AlterWorkloadGroupStmt{} + + // Group name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropWorkloadGroup parses: +// +// DROP WORKLOAD GROUP [IF EXISTS] name +// +// DROP has already been consumed; cur is WORKLOAD. The WORKLOAD and GROUP +// tokens are consumed here. +func (p *Parser) parseDropWorkloadGroup(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwGROUP); err != nil { + return nil, err + } + + stmt := &ast.DropWorkloadGroupStmt{} + + // Optional IF EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfExists = true + } + + // Group name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// WORKLOAD POLICY +// --------------------------------------------------------------------------- + +// parseWorkloadPolicyItemList parses a parenthesised list of items for +// CONDITIONS or ACTIONS. The list contents are captured as raw text because +// the individual condition/action syntax is complex and varied. Each comma- +// separated top-level item is returned as a separate WorkloadPolicyItem. +// +// (item1, item2, ...) +// +// cur must be '(' on entry; it is consumed here. +func (p *Parser) parseWorkloadPolicyItemList() ([]*ast.WorkloadPolicyItem, error) { + openTok, err := p.expect(int('(')) + if err != nil { + return nil, err + } + _ = openTok + + var items []*ast.WorkloadPolicyItem + var buf strings.Builder + itemStart := p.cur.Loc + + depth := 0 + for p.cur.Kind != tokEOF { + if p.cur.Kind == int(')') && depth == 0 { + break + } + switch p.cur.Kind { + case int('('): + depth++ + case int(')'): + depth-- + case int(','): + if depth == 0 { + // End of this item — save it and start a fresh one. + raw := strings.TrimSpace(buf.String()) + items = append(items, &ast.WorkloadPolicyItem{ + RawText: raw, + Loc: ast.Loc{Start: itemStart.Start, End: p.prev.Loc.End}, + }) + buf.Reset() + p.advance() // consume ',' + itemStart = p.cur.Loc + continue + } + } + if buf.Len() > 0 { + buf.WriteByte(' ') + } + if p.cur.Str != "" { + buf.WriteString(p.cur.Str) + } else { + buf.WriteString(TokenName(p.cur.Kind)) + } + p.advance() + } + + // Capture the last (or only) item before the closing ')'. + raw := strings.TrimSpace(buf.String()) + if raw != "" { + items = append(items, &ast.WorkloadPolicyItem{ + RawText: raw, + Loc: ast.Loc{Start: itemStart.Start, End: p.cur.Loc.Start}, + }) + } + + if _, err := p.expect(int(')')); err != nil { + return nil, err + } + return items, nil +} + +// parseCreateWorkloadPolicy parses: +// +// CREATE WORKLOAD POLICY [IF NOT EXISTS] name +// CONDITIONS(condition_list) +// ACTIONS(action_list) +// [PROPERTIES(...)] +// +// CREATE has already been consumed; cur is WORKLOAD. The WORKLOAD and POLICY +// tokens are consumed here. +func (p *Parser) parseCreateWorkloadPolicy(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwPOLICY); err != nil { + return nil, err + } + + stmt := &ast.CreateWorkloadPolicyStmt{} + + // Optional IF NOT EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwNOT); err != nil { + return nil, err + } + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfNotExists = true + } + + // Policy name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // Optional CONDITIONS(...) + if p.cur.Kind == kwCONDITIONS { + p.advance() // consume CONDITIONS + conds, err := p.parseWorkloadPolicyItemList() + if err != nil { + return nil, err + } + stmt.Conditions = conds + } + + // Optional ACTIONS(...) + if p.cur.Kind == kwACTIONS { + p.advance() // consume ACTIONS + actions, err := p.parseWorkloadPolicyItemList() + if err != nil { + return nil, err + } + stmt.Actions = actions + } + + // Optional PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAlterWorkloadPolicy parses: +// +// ALTER WORKLOAD POLICY name PROPERTIES(...) +// +// ALTER has already been consumed; cur is WORKLOAD. The WORKLOAD and POLICY +// tokens are consumed here. +func (p *Parser) parseAlterWorkloadPolicy(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwPOLICY); err != nil { + return nil, err + } + + stmt := &ast.AlterWorkloadPolicyStmt{} + + // Policy name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropWorkloadPolicy parses: +// +// DROP WORKLOAD POLICY [IF EXISTS] name +// +// DROP has already been consumed; cur is WORKLOAD. The WORKLOAD and POLICY +// tokens are consumed here. +func (p *Parser) parseDropWorkloadPolicy(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume WORKLOAD + if _, err := p.expect(kwPOLICY); err != nil { + return nil, err + } + + stmt := &ast.DropWorkloadPolicyStmt{} + + // Optional IF EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfExists = true + } + + // Policy name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// RESOURCE +// --------------------------------------------------------------------------- + +// parseCreateResource parses: +// +// CREATE [EXTERNAL] RESOURCE [IF NOT EXISTS] name PROPERTIES(...) +// +// CREATE has already been consumed; cur is RESOURCE (for non-EXTERNAL) or +// EXTERNAL (the caller already peeked). The RESOURCE (and optional EXTERNAL) +// token(s) are consumed here. external indicates whether EXTERNAL was already +// seen/consumed by the caller. +func (p *Parser) parseCreateResource(startLoc ast.Loc, external bool) (ast.Node, error) { + // cur is RESOURCE + if _, err := p.expect(kwRESOURCE); err != nil { + return nil, err + } + + stmt := &ast.CreateResourceStmt{External: external} + + // Optional IF NOT EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwNOT); err != nil { + return nil, err + } + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfNotExists = true + } + + // Resource name — can be a string literal or identifier + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAlterResource parses: +// +// ALTER RESOURCE name PROPERTIES(...) +// +// ALTER has already been consumed; cur is RESOURCE. RESOURCE is consumed here. +func (p *Parser) parseAlterResource(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume RESOURCE + + stmt := &ast.AlterResourceStmt{} + + // Resource name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropResource parses: +// +// DROP RESOURCE [IF EXISTS] name +// +// DROP has already been consumed; cur is RESOURCE. RESOURCE is consumed here. +func (p *Parser) parseDropResource(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume RESOURCE + + stmt := &ast.DropResourceStmt{} + + // Optional IF EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfExists = true + } + + // Resource name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// SQL BLOCK RULE +// --------------------------------------------------------------------------- + +// parseCreateSQLBlockRule parses: +// +// CREATE SQL_BLOCK_RULE [IF NOT EXISTS] name PROPERTIES(...) +// +// CREATE has already been consumed; cur is SQL_BLOCK_RULE. SQL_BLOCK_RULE is +// consumed here. +func (p *Parser) parseCreateSQLBlockRule(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume SQL_BLOCK_RULE + + stmt := &ast.CreateSQLBlockRuleStmt{} + + // Optional IF NOT EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwNOT); err != nil { + return nil, err + } + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfNotExists = true + } + + // Rule name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAlterSQLBlockRule parses: +// +// ALTER SQL_BLOCK_RULE name PROPERTIES(...) +// +// ALTER has already been consumed; cur is SQL_BLOCK_RULE. SQL_BLOCK_RULE is +// consumed here. +func (p *Parser) parseAlterSQLBlockRule(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume SQL_BLOCK_RULE + + stmt := &ast.AlterSQLBlockRuleStmt{} + + // Rule name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // PROPERTIES clause + if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropSQLBlockRule parses: +// +// DROP SQL_BLOCK_RULE [IF EXISTS] name +// +// DROP has already been consumed; cur is SQL_BLOCK_RULE. SQL_BLOCK_RULE is +// consumed here. +func (p *Parser) parseDropSQLBlockRule(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume SQL_BLOCK_RULE + + stmt := &ast.DropSQLBlockRuleStmt{} + + // Optional IF EXISTS + if p.cur.Kind == kwIF { + p.advance() // consume IF + if _, err := p.expect(kwEXISTS); err != nil { + return nil, err + } + stmt.IfExists = true + } + + // Rule name + name, nameLoc, err := p.parseIdentifierOrString() + if err != nil { + return nil, err + } + stmt.Name = name + + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} diff --git a/doris/parser/workload_test.go b/doris/parser/workload_test.go new file mode 100644 index 00000000..dfc6f3d9 --- /dev/null +++ b/doris/parser/workload_test.go @@ -0,0 +1,819 @@ +package parser + +import ( + "testing" + + "github.com/bytebase/omni/doris/ast" +) + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP — CREATE +// --------------------------------------------------------------------------- + +func TestCreateWorkloadGroup_Basic(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD GROUP g1 PROPERTIES ("max_cpu_percent"="10%")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateWorkloadGroupStmt) + if !ok { + t.Fatalf("expected *ast.CreateWorkloadGroupStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "g1" { + t.Errorf("Name = %q, want %q", stmt.Name, "g1") + } + if stmt.IfNotExists { + t.Error("IfNotExists should be false") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "max_cpu_percent" { + t.Errorf("Properties[0].Key = %q, want %q", stmt.Properties[0].Key, "max_cpu_percent") + } + if stmt.Properties[0].Value != "10%" { + t.Errorf("Properties[0].Value = %q, want %q", stmt.Properties[0].Value, "10%") + } +} + +func TestCreateWorkloadGroup_IfNotExists(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD GROUP IF NOT EXISTS g1 PROPERTIES ( + "max_cpu_percent"="10%", + "max_memory_percent"="30%" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateWorkloadGroupStmt) + if !stmt.IfNotExists { + t.Error("IfNotExists should be true") + } + if stmt.Name != "g1" { + t.Errorf("Name = %q, want %q", stmt.Name, "g1") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateWorkloadGroup_Tag(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD GROUP g1 PROPERTIES ("max_cpu_percent"="10%")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateWorkloadGroupStmt { + t.Errorf("Tag() = %v, want T_CreateWorkloadGroupStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP — ALTER +// --------------------------------------------------------------------------- + +func TestAlterWorkloadGroup_Basic(t *testing.T) { + file, errs := Parse(`ALTER WORKLOAD GROUP g1 PROPERTIES ( + "max_cpu_percent"="20%", + "max_memory_percent"="40%" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.AlterWorkloadGroupStmt) + if !ok { + t.Fatalf("expected *ast.AlterWorkloadGroupStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "g1" { + t.Errorf("Name = %q, want %q", stmt.Name, "g1") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestAlterWorkloadGroup_Tag(t *testing.T) { + file, errs := Parse(`ALTER WORKLOAD GROUP g1 PROPERTIES ("max_cpu_percent"="20%")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_AlterWorkloadGroupStmt { + t.Errorf("Tag() = %v, want T_AlterWorkloadGroupStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP — DROP +// --------------------------------------------------------------------------- + +func TestDropWorkloadGroup_Basic(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD GROUP g1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.DropWorkloadGroupStmt) + if !ok { + t.Fatalf("expected *ast.DropWorkloadGroupStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "g1" { + t.Errorf("Name = %q, want %q", stmt.Name, "g1") + } + if stmt.IfExists { + t.Error("IfExists should be false") + } +} + +func TestDropWorkloadGroup_IfExists(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD GROUP IF EXISTS g1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropWorkloadGroupStmt) + if !stmt.IfExists { + t.Error("IfExists should be true") + } + if stmt.Name != "g1" { + t.Errorf("Name = %q, want %q", stmt.Name, "g1") + } +} + +func TestDropWorkloadGroup_Tag(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD GROUP IF EXISTS g1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_DropWorkloadGroupStmt { + t.Errorf("Tag() = %v, want T_DropWorkloadGroupStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD GROUP — legacy corpus +// --------------------------------------------------------------------------- + +func TestWorkloadGroup_LegacyCorpus(t *testing.T) { + // From cluster_workload_group.sql + cases := []struct { + sql string + want interface{} + }{ + { + sql: `CREATE WORKLOAD GROUP IF NOT EXISTS g1 PROPERTIES ("max_cpu_percent"="10%", "max_memory_percent"="30%")`, + want: (*ast.CreateWorkloadGroupStmt)(nil), + }, + { + sql: `ALTER WORKLOAD GROUP g1 PROPERTIES ("max_cpu_percent"="20%", "max_memory_percent"="40%")`, + want: (*ast.AlterWorkloadGroupStmt)(nil), + }, + { + sql: `DROP WORKLOAD GROUP IF EXISTS g1`, + want: (*ast.DropWorkloadGroupStmt)(nil), + }, + } + + for _, tc := range cases { + t.Run(tc.sql[:20], func(t *testing.T) { + file, errs := Parse(tc.sql) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + }) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD POLICY — CREATE +// --------------------------------------------------------------------------- + +func TestCreateWorkloadPolicy_Basic(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD POLICY p1 + CONDITIONS(query_time > 1000) + ACTIONS(cancel_query) + PROPERTIES("enabled"="true")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateWorkloadPolicyStmt) + if !ok { + t.Fatalf("expected *ast.CreateWorkloadPolicyStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "p1" { + t.Errorf("Name = %q, want %q", stmt.Name, "p1") + } + if stmt.IfNotExists { + t.Error("IfNotExists should be false") + } + if len(stmt.Conditions) != 1 { + t.Errorf("expected 1 condition, got %d", len(stmt.Conditions)) + } + if len(stmt.Actions) != 1 { + t.Errorf("expected 1 action, got %d", len(stmt.Actions)) + } + if len(stmt.Properties) != 1 { + t.Errorf("expected 1 property, got %d", len(stmt.Properties)) + } +} + +func TestCreateWorkloadPolicy_IfNotExists(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD POLICY IF NOT EXISTS p1 + CONDITIONS(scan_rows > 1000000000, query_time > 1000) + ACTIONS(cancel_query)`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateWorkloadPolicyStmt) + if !stmt.IfNotExists { + t.Error("IfNotExists should be true") + } + if len(stmt.Conditions) != 2 { + t.Errorf("expected 2 conditions, got %d", len(stmt.Conditions)) + } + if len(stmt.Actions) != 1 { + t.Errorf("expected 1 action, got %d", len(stmt.Actions)) + } +} + +func TestCreateWorkloadPolicy_Tag(t *testing.T) { + file, errs := Parse(`CREATE WORKLOAD POLICY p1 CONDITIONS(query_time > 1000) ACTIONS(cancel_query)`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateWorkloadPolicyStmt { + t.Errorf("Tag() = %v, want T_CreateWorkloadPolicyStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD POLICY — ALTER +// --------------------------------------------------------------------------- + +func TestAlterWorkloadPolicy_Basic(t *testing.T) { + file, errs := Parse(`ALTER WORKLOAD POLICY p1 PROPERTIES("enabled"="false")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.AlterWorkloadPolicyStmt) + if !ok { + t.Fatalf("expected *ast.AlterWorkloadPolicyStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "p1" { + t.Errorf("Name = %q, want %q", stmt.Name, "p1") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "enabled" { + t.Errorf("Properties[0].Key = %q, want %q", stmt.Properties[0].Key, "enabled") + } +} + +func TestAlterWorkloadPolicy_Tag(t *testing.T) { + file, errs := Parse(`ALTER WORKLOAD POLICY p1 PROPERTIES("enabled"="false")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_AlterWorkloadPolicyStmt { + t.Errorf("Tag() = %v, want T_AlterWorkloadPolicyStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// WORKLOAD POLICY — DROP +// --------------------------------------------------------------------------- + +func TestDropWorkloadPolicy_Basic(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD POLICY p1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.DropWorkloadPolicyStmt) + if !ok { + t.Fatalf("expected *ast.DropWorkloadPolicyStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "p1" { + t.Errorf("Name = %q, want %q", stmt.Name, "p1") + } + if stmt.IfExists { + t.Error("IfExists should be false") + } +} + +func TestDropWorkloadPolicy_IfExists(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD POLICY IF EXISTS p1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropWorkloadPolicyStmt) + if !stmt.IfExists { + t.Error("IfExists should be true") + } +} + +func TestDropWorkloadPolicy_Tag(t *testing.T) { + file, errs := Parse(`DROP WORKLOAD POLICY IF EXISTS p1`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_DropWorkloadPolicyStmt { + t.Errorf("Tag() = %v, want T_DropWorkloadPolicyStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// RESOURCE — CREATE (non-EXTERNAL) +// --------------------------------------------------------------------------- + +func TestCreateResource_Basic(t *testing.T) { + file, errs := Parse(`CREATE RESOURCE mysql_resource PROPERTIES ( + "type"="jdbc", + "user"="root", + "password"="123456" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateResourceStmt) + if !ok { + t.Fatalf("expected *ast.CreateResourceStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "mysql_resource" { + t.Errorf("Name = %q, want %q", stmt.Name, "mysql_resource") + } + if stmt.External { + t.Error("External should be false") + } + if stmt.IfNotExists { + t.Error("IfNotExists should be false") + } + if len(stmt.Properties) != 3 { + t.Fatalf("expected 3 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateResource_QuotedName(t *testing.T) { + file, errs := Parse(`CREATE RESOURCE "remote_s3" PROPERTIES( + "type"="s3", + "s3.endpoint"="bj.s3.com" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateResourceStmt) + if stmt.Name != "remote_s3" { + t.Errorf("Name = %q, want %q", stmt.Name, "remote_s3") + } + if stmt.External { + t.Error("External should be false") + } +} + +func TestCreateResource_IfNotExists(t *testing.T) { + file, errs := Parse(`CREATE RESOURCE IF NOT EXISTS r1 PROPERTIES("type"="s3")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateResourceStmt) + if !stmt.IfNotExists { + t.Error("IfNotExists should be true") + } +} + +func TestCreateResource_Tag(t *testing.T) { + file, errs := Parse(`CREATE RESOURCE r1 PROPERTIES("type"="s3")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateResourceStmt { + t.Errorf("Tag() = %v, want T_CreateResourceStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// RESOURCE — CREATE EXTERNAL +// --------------------------------------------------------------------------- + +func TestCreateExternalResource_Basic(t *testing.T) { + file, errs := Parse(`CREATE EXTERNAL RESOURCE "spark0" + PROPERTIES( + "type" = "spark", + "spark.master" = "yarn", + "spark.submit.deployMode" = "cluster", + "working_dir" = "hdfs://127.0.0.1:10000/tmp/doris", + "broker" = "broker0" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateResourceStmt) + if !ok { + t.Fatalf("expected *ast.CreateResourceStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "spark0" { + t.Errorf("Name = %q, want %q", stmt.Name, "spark0") + } + if !stmt.External { + t.Error("External should be true") + } + if len(stmt.Properties) != 5 { + t.Fatalf("expected 5 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateExternalResource_BacktickName(t *testing.T) { + file, errs := Parse("CREATE EXTERNAL RESOURCE `oracle_odbc` PROPERTIES (\"type\" = \"odbc_catalog\", \"host\" = \"192.168.0.1\")") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateResourceStmt) + if stmt.Name != "oracle_odbc" { + t.Errorf("Name = %q, want %q", stmt.Name, "oracle_odbc") + } + if !stmt.External { + t.Error("External should be true") + } +} + +func TestCreateExternalResource_Tag(t *testing.T) { + file, errs := Parse(`CREATE EXTERNAL RESOURCE "spark0" PROPERTIES("type"="spark")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateResourceStmt { + t.Errorf("Tag() = %v, want T_CreateResourceStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// RESOURCE — ALTER +// --------------------------------------------------------------------------- + +func TestAlterResource_Basic(t *testing.T) { + file, errs := Parse(`ALTER RESOURCE spark0 PROPERTIES("spark.executor.memory"="2g")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.AlterResourceStmt) + if !ok { + t.Fatalf("expected *ast.AlterResourceStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "spark0" { + t.Errorf("Name = %q, want %q", stmt.Name, "spark0") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } +} + +func TestAlterResource_Tag(t *testing.T) { + file, errs := Parse(`ALTER RESOURCE r1 PROPERTIES("type"="s3")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_AlterResourceStmt { + t.Errorf("Tag() = %v, want T_AlterResourceStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// RESOURCE — DROP +// --------------------------------------------------------------------------- + +func TestDropResource_Basic(t *testing.T) { + file, errs := Parse(`DROP RESOURCE 'spark0'`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.DropResourceStmt) + if !ok { + t.Fatalf("expected *ast.DropResourceStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "spark0" { + t.Errorf("Name = %q, want %q", stmt.Name, "spark0") + } + if stmt.IfExists { + t.Error("IfExists should be false") + } +} + +func TestDropResource_IfExists(t *testing.T) { + file, errs := Parse(`DROP RESOURCE IF EXISTS spark0`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropResourceStmt) + if !stmt.IfExists { + t.Error("IfExists should be true") + } +} + +func TestDropResource_Tag(t *testing.T) { + file, errs := Parse(`DROP RESOURCE 'spark0'`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_DropResourceStmt { + t.Errorf("Tag() = %v, want T_DropResourceStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// RESOURCE — legacy corpus +// --------------------------------------------------------------------------- + +func TestResource_LegacyCorpus(t *testing.T) { + cases := []string{ + `CREATE EXTERNAL RESOURCE "spark0" + PROPERTIES( + "type" = "spark", + "spark.master" = "yarn", + "spark.submit.deployMode" = "cluster", + "spark.jars" = "xxx.jar,yyy.jar", + "spark.files" = "/tmp/aaa,/tmp/bbb", + "spark.executor.memory" = "1g", + "spark.yarn.queue" = "queue0", + "spark.hadoop.yarn.resourcemanager.address" = "127.0.0.1:9999", + "spark.hadoop.fs.defaultFS" = "hdfs://127.0.0.1:10000", + "working_dir" = "hdfs://127.0.0.1:10000/tmp/doris", + "broker" = "broker0", + "broker.username" = "user0", + "broker.password" = "password0" + )`, + `CREATE RESOURCE "remote_s3" + PROPERTIES( + "type" = "s3", + "s3.endpoint" = "bj.s3.com", + "s3.region" = "bj", + "s3.access_key" = "bbb", + "s3.secret_key" = "aaaa", + "s3.connection.maximum" = "50", + "s3.connection.request.timeout" = "3000", + "s3.connection.timeout" = "1000" + )`, + `CREATE RESOURCE mysql_resource PROPERTIES ( + "type"="jdbc", + "user"="root", + "password"="123456", + "jdbc_url" = "jdbc:mysql://127.0.0.1:3316/doris_test?useSSL=false", + "driver_url" = "https://doris-community-test-1308700295.cos.ap-hongkong.myqcloud.com/jdbc_driver/mysql-connector-java-8.0.25.jar", + "driver_class" = "com.mysql.cj.jdbc.Driver" + )`, + `DROP RESOURCE 'spark0'`, + } + + for i, sql := range cases { + file, errs := Parse(sql) + if len(errs) != 0 { + t.Errorf("case %d: unexpected errors: %v", i, errs) + continue + } + if len(file.Stmts) != 1 { + t.Errorf("case %d: expected 1 stmt, got %d", i, len(file.Stmts)) + } + } +} + +// --------------------------------------------------------------------------- +// SQL BLOCK RULE — CREATE +// --------------------------------------------------------------------------- + +func TestCreateSQLBlockRule_Basic(t *testing.T) { + file, errs := Parse(`CREATE SQL_BLOCK_RULE test_rule PROPERTIES ( + "sql"="select \\* from order_analysis", + "enable"="true" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateSQLBlockRuleStmt) + if !ok { + t.Fatalf("expected *ast.CreateSQLBlockRuleStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "test_rule" { + t.Errorf("Name = %q, want %q", stmt.Name, "test_rule") + } + if stmt.IfNotExists { + t.Error("IfNotExists should be false") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateSQLBlockRule_IfNotExists(t *testing.T) { + file, errs := Parse(`CREATE SQL_BLOCK_RULE IF NOT EXISTS test_rule PROPERTIES("sql"="select 1", "enable"="true")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateSQLBlockRuleStmt) + if !stmt.IfNotExists { + t.Error("IfNotExists should be true") + } +} + +func TestCreateSQLBlockRule_ScanRowsLimit(t *testing.T) { + file, errs := Parse(`CREATE SQL_BLOCK_RULE test_rule2 PROPERTIES ( + "scan_row_limit"="100", + "enable"="true" + )`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateSQLBlockRuleStmt) + if stmt.Name != "test_rule2" { + t.Errorf("Name = %q, want %q", stmt.Name, "test_rule2") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateSQLBlockRule_Tag(t *testing.T) { + file, errs := Parse(`CREATE SQL_BLOCK_RULE r1 PROPERTIES("sql"="select 1", "enable"="true")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateSQLBlockRuleStmt { + t.Errorf("Tag() = %v, want T_CreateSQLBlockRuleStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// SQL BLOCK RULE — ALTER +// --------------------------------------------------------------------------- + +func TestAlterSQLBlockRule_Basic(t *testing.T) { + file, errs := Parse(`ALTER SQL_BLOCK_RULE test_rule PROPERTIES("enable"="false")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.AlterSQLBlockRuleStmt) + if !ok { + t.Fatalf("expected *ast.AlterSQLBlockRuleStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "test_rule" { + t.Errorf("Name = %q, want %q", stmt.Name, "test_rule") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } +} + +func TestAlterSQLBlockRule_Tag(t *testing.T) { + file, errs := Parse(`ALTER SQL_BLOCK_RULE r1 PROPERTIES("enable"="false")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_AlterSQLBlockRuleStmt { + t.Errorf("Tag() = %v, want T_AlterSQLBlockRuleStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// SQL BLOCK RULE — DROP +// --------------------------------------------------------------------------- + +func TestDropSQLBlockRule_Basic(t *testing.T) { + file, errs := Parse(`DROP SQL_BLOCK_RULE test_rule`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.DropSQLBlockRuleStmt) + if !ok { + t.Fatalf("expected *ast.DropSQLBlockRuleStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "test_rule" { + t.Errorf("Name = %q, want %q", stmt.Name, "test_rule") + } + if stmt.IfExists { + t.Error("IfExists should be false") + } +} + +func TestDropSQLBlockRule_IfExists(t *testing.T) { + file, errs := Parse(`DROP SQL_BLOCK_RULE IF EXISTS test_rule`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropSQLBlockRuleStmt) + if !stmt.IfExists { + t.Error("IfExists should be true") + } +} + +func TestDropSQLBlockRule_Tag(t *testing.T) { + file, errs := Parse(`DROP SQL_BLOCK_RULE IF EXISTS test_rule`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_DropSQLBlockRuleStmt { + t.Errorf("Tag() = %v, want T_DropSQLBlockRuleStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// Multi-statement round-trip +// --------------------------------------------------------------------------- + +func TestWorkload_MultiStatement(t *testing.T) { + input := `CREATE WORKLOAD GROUP IF NOT EXISTS g1 PROPERTIES ("max_cpu_percent"="10%", "max_memory_percent"="30%"); +ALTER WORKLOAD GROUP g1 PROPERTIES ("max_cpu_percent"="20%", "max_memory_percent"="40%"); +DROP WORKLOAD GROUP IF EXISTS g1` + + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 3 { + t.Fatalf("expected 3 stmts, got %d", len(file.Stmts)) + } + if _, ok := file.Stmts[0].(*ast.CreateWorkloadGroupStmt); !ok { + t.Errorf("Stmts[0]: expected *CreateWorkloadGroupStmt, got %T", file.Stmts[0]) + } + if _, ok := file.Stmts[1].(*ast.AlterWorkloadGroupStmt); !ok { + t.Errorf("Stmts[1]: expected *AlterWorkloadGroupStmt, got %T", file.Stmts[1]) + } + if _, ok := file.Stmts[2].(*ast.DropWorkloadGroupStmt); !ok { + t.Errorf("Stmts[2]: expected *DropWorkloadGroupStmt, got %T", file.Stmts[2]) + } +} + +func TestResource_MultiStatement(t *testing.T) { + input := `CREATE EXTERNAL RESOURCE "spark0" PROPERTIES("type"="spark"); +CREATE RESOURCE "remote_s3" PROPERTIES("type"="s3"); +DROP RESOURCE 'spark0'` + + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 3 { + t.Fatalf("expected 3 stmts, got %d", len(file.Stmts)) + } + stmt0 := file.Stmts[0].(*ast.CreateResourceStmt) + if !stmt0.External { + t.Error("Stmts[0]: External should be true") + } + stmt1 := file.Stmts[1].(*ast.CreateResourceStmt) + if stmt1.External { + t.Error("Stmts[1]: External should be false") + } +} + +// --------------------------------------------------------------------------- +// Existing CREATE EXTERNAL TABLE still works +// --------------------------------------------------------------------------- + +func TestCreateExternalTable_StillWorks(t *testing.T) { + // CREATE EXTERNAL TABLE should still route to parseCreateTable, not parseCreateResource. + file, errs := Parse("CREATE EXTERNAL TABLE t (id INT) ENGINE=HIVE PROPERTIES(\"database\"=\"db\", \"table\"=\"t\")") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(file.Stmts)) + } + stmt, ok := file.Stmts[0].(*ast.CreateTableStmt) + if !ok { + t.Fatalf("expected *ast.CreateTableStmt, got %T", file.Stmts[0]) + } + if !stmt.External { + t.Error("External should be true on CreateTableStmt") + } +}