diff --git a/doris/ast/catalognodes.go b/doris/ast/catalognodes.go new file mode 100644 index 00000000..008de2f4 --- /dev/null +++ b/doris/ast/catalognodes.go @@ -0,0 +1,89 @@ +package ast + +// This file holds DDL AST node types for CATALOG statements (T5.2). +// +// Supported forms: +// CREATE [EXTERNAL] CATALOG [IF NOT EXISTS] name [COMMENT '...'] [PROPERTIES(...)] [WITH RESOURCE name] +// ALTER CATALOG name { RENAME new_name | SET PROPERTIES (...) | MODIFY COMMENT 'text' | PROPERTY (...) } +// DROP CATALOG [IF EXISTS] name +// REFRESH CATALOG name [PROPERTIES(...)] + +// CreateCatalogStmt represents: +// +// CREATE [EXTERNAL] CATALOG [IF NOT EXISTS] catalog_name +// [COMMENT 'comment'] +// [PROPERTIES ("key"="value", ...)] +// [WITH RESOURCE resource_name] +type CreateCatalogStmt struct { + Name string + External bool + IfNotExists bool + Comment string + Properties []*Property + WithResource string // non-empty when WITH RESOURCE resource_name is present + Loc Loc +} + +// Tag implements Node. +func (n *CreateCatalogStmt) Tag() NodeTag { return T_CreateCatalogStmt } + +var _ Node = (*CreateCatalogStmt)(nil) + +// AlterCatalogAction identifies the kind of action in an ALTER CATALOG statement. +type AlterCatalogAction int + +const ( + AlterCatalogRename AlterCatalogAction = iota // RENAME new_name + AlterCatalogSetProperties // SET PROPERTIES (...) + AlterCatalogModifyComment // MODIFY COMMENT 'text' + AlterCatalogSetProperty // PROPERTY ("key"="value") +) + +// AlterCatalogStmt represents: +// +// ALTER CATALOG name +// { RENAME new_name +// | SET PROPERTIES ("key"="value", ...) +// | MODIFY COMMENT 'text' +// | PROPERTY ("key"="value") } +type AlterCatalogStmt struct { + Name string + Action AlterCatalogAction + NewName string // for RENAME + Properties []*Property // for SET PROPERTIES / PROPERTY + Comment string // for MODIFY COMMENT + Loc Loc +} + +// Tag implements Node. +func (n *AlterCatalogStmt) Tag() NodeTag { return T_AlterCatalogStmt } + +var _ Node = (*AlterCatalogStmt)(nil) + +// DropCatalogStmt represents: +// +// DROP CATALOG [IF EXISTS] catalog_name +type DropCatalogStmt struct { + Name string + IfExists bool + Loc Loc +} + +// Tag implements Node. +func (n *DropCatalogStmt) Tag() NodeTag { return T_DropCatalogStmt } + +var _ Node = (*DropCatalogStmt)(nil) + +// RefreshCatalogStmt represents: +// +// REFRESH CATALOG catalog_name [PROPERTIES(...)] +type RefreshCatalogStmt struct { + Name string + Properties []*Property + Loc Loc +} + +// Tag implements Node. +func (n *RefreshCatalogStmt) Tag() NodeTag { return T_RefreshCatalogStmt } + +var _ Node = (*RefreshCatalogStmt)(nil) diff --git a/doris/ast/loc.go b/doris/ast/loc.go index cfc31c2d..9596155c 100644 --- a/doris/ast/loc.go +++ b/doris/ast/loc.go @@ -124,6 +124,14 @@ func NodeLoc(n Node) Loc { return v.Loc case *MergeClause: return v.Loc + case *CreateCatalogStmt: + return v.Loc + case *AlterCatalogStmt: + return v.Loc + case *DropCatalogStmt: + return v.Loc + case *RefreshCatalogStmt: + return v.Loc default: return NoLoc() } diff --git a/doris/ast/nodetags.go b/doris/ast/nodetags.go index 1cc7c3e6..8ea2761d 100644 --- a/doris/ast/nodetags.go +++ b/doris/ast/nodetags.go @@ -206,6 +206,20 @@ const ( // T_MergeClause is the tag for *MergeClause (one WHEN clause inside MERGE). T_MergeClause + + // DDL — CATALOG nodes (T5.2). + + // T_CreateCatalogStmt is the tag for *CreateCatalogStmt. + T_CreateCatalogStmt + + // T_AlterCatalogStmt is the tag for *AlterCatalogStmt. + T_AlterCatalogStmt + + // T_DropCatalogStmt is the tag for *DropCatalogStmt. + T_DropCatalogStmt + + // T_RefreshCatalogStmt is the tag for *RefreshCatalogStmt. + T_RefreshCatalogStmt ) // String returns a human-readable representation of the tag. @@ -327,6 +341,14 @@ func (t NodeTag) String() string { return "MergeStmt" case T_MergeClause: return "MergeClause" + case T_CreateCatalogStmt: + return "CreateCatalogStmt" + case T_AlterCatalogStmt: + return "AlterCatalogStmt" + case T_DropCatalogStmt: + return "DropCatalogStmt" + case T_RefreshCatalogStmt: + return "RefreshCatalogStmt" default: return "Unknown" } diff --git a/doris/ast/walk_children.go b/doris/ast/walk_children.go index e3b3c3c7..00117577 100644 --- a/doris/ast/walk_children.go +++ b/doris/ast/walk_children.go @@ -348,5 +348,21 @@ func walkChildren(v Visitor, node Node) { Walk(v, val) } } + + // DDL — CATALOG nodes (T5.2). + case *CreateCatalogStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *AlterCatalogStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } + case *DropCatalogStmt: + // leaf-ish node, name stored as string + case *RefreshCatalogStmt: + for _, prop := range n.Properties { + Walk(v, prop) + } } } diff --git a/doris/parser/catalog.go b/doris/parser/catalog.go new file mode 100644 index 00000000..cee9be56 --- /dev/null +++ b/doris/parser/catalog.go @@ -0,0 +1,301 @@ +package parser + +import ( + "github.com/bytebase/omni/doris/ast" +) + +// parseCreateCatalog parses: +// +// CREATE [EXTERNAL] CATALOG [IF NOT EXISTS] catalog_name +// [COMMENT 'comment'] +// [PROPERTIES ("key"="value", ...)] +// [WITH RESOURCE resource_name] +// +// The CREATE keyword has already been consumed by the caller. +// cur may be kwEXTERNAL or kwCATALOG on entry. +func (p *Parser) parseCreateCatalog() (ast.Node, error) { + startLoc := p.prev.Loc // loc of CREATE token + + stmt := &ast.CreateCatalogStmt{} + + // Optional EXTERNAL + if p.cur.Kind == kwEXTERNAL { + stmt.External = true + p.advance() + } + + // CATALOG keyword + if _, err := p.expect(kwCATALOG); err != nil { + return nil, err + } + + // 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 + } + + // Catalog name — single identifier + name, nameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // Optional clauses: COMMENT, PROPERTIES, WITH RESOURCE (in any order per Doris docs) + for { + switch p.cur.Kind { + case kwCOMMENT: + p.advance() // consume COMMENT + if p.cur.Kind != tokString { + return nil, p.syntaxErrorAtCur() + } + stmt.Comment = p.cur.Str + endLoc = p.cur.Loc + p.advance() + case 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]) + } + case kwWITH: + p.advance() // consume WITH + if _, err := p.expect(kwRESOURCE); err != nil { + return nil, err + } + resourceName, resourceLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.WithResource = resourceName + endLoc = resourceLoc + default: + goto done + } + } +done: + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAlterCatalog parses: +// +// ALTER CATALOG catalog_name +// { RENAME new_name +// | SET PROPERTIES ("key"="value", ...) +// | MODIFY COMMENT 'text' +// | PROPERTY ("key"="value") } +// +// The ALTER keyword has already been consumed; cur is CATALOG. +func (p *Parser) parseAlterCatalog() (ast.Node, error) { + startLoc := p.prev.Loc // loc of ALTER token + + // Consume CATALOG + p.advance() + + stmt := &ast.AlterCatalogStmt{} + + // Catalog name — single identifier + name, _, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + + endLoc := startLoc + + switch p.cur.Kind { + case kwRENAME: + p.advance() // consume RENAME + newName, newNameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Action = ast.AlterCatalogRename + stmt.NewName = newName + endLoc = newNameLoc + + case kwSET: + p.advance() // consume SET + if p.cur.Kind != kwPROPERTIES { + return nil, p.syntaxErrorAtCur() + } + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Action = ast.AlterCatalogSetProperties + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + + case kwMODIFY: + p.advance() // consume MODIFY + if _, err := p.expect(kwCOMMENT); err != nil { + return nil, err + } + if p.cur.Kind != tokString { + return nil, p.syntaxErrorAtCur() + } + stmt.Action = ast.AlterCatalogModifyComment + stmt.Comment = p.cur.Str + endLoc = p.cur.Loc + p.advance() + + case kwPROPERTY: + // PROPERTY ("key"="value") — singular, no SET prefix + props, err := p.parsePropertyClause() + if err != nil { + return nil, err + } + stmt.Action = ast.AlterCatalogSetProperty + stmt.Properties = props + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + + default: + return nil, p.syntaxErrorAtCur() + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropCatalog parses: +// +// DROP CATALOG [IF EXISTS] catalog_name +// +// The DROP keyword has already been consumed; cur is CATALOG. +func (p *Parser) parseDropCatalog() (ast.Node, error) { + startLoc := p.prev.Loc // loc of DROP token + + // Consume CATALOG + p.advance() + + stmt := &ast.DropCatalogStmt{} + + // 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 + } + + // Catalog name — single identifier + name, nameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} + +// parseRefreshCatalog parses: +// +// REFRESH CATALOG catalog_name [PROPERTIES(...)] +// +// The REFRESH keyword has already been consumed; cur is CATALOG. +func (p *Parser) parseRefreshCatalog() (ast.Node, error) { + startLoc := p.prev.Loc // loc of REFRESH token + + // Consume CATALOG + p.advance() + + stmt := &ast.RefreshCatalogStmt{} + + // Catalog name — single identifier + name, nameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc := nameLoc + + // Optional PROPERTIES + 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 +} + +// parsePropertyClause parses: +// +// PROPERTY ("key"="value" [, "key"="value" ...]) +// +// cur must be kwPROPERTY on entry; it is consumed here. +// This is the singular form used in ALTER CATALOG ... PROPERTY (...). +func (p *Parser) parsePropertyClause() ([]*ast.Property, error) { + p.advance() // consume PROPERTY + + if _, err := p.expect(int('(')); err != nil { + return nil, err + } + + var props []*ast.Property + + for p.cur.Kind != int(')') && p.cur.Kind != tokEOF { + startLoc := p.cur.Loc + + // Key — must be a string literal + if p.cur.Kind != tokString { + return nil, p.syntaxErrorAtCur() + } + key := p.cur.Str + p.advance() + + // '=' + if _, err := p.expect(int('=')); err != nil { + return nil, err + } + + // Value — must be a string literal + if p.cur.Kind != tokString { + return nil, p.syntaxErrorAtCur() + } + val := p.cur.Str + endLoc := p.cur.Loc + p.advance() + + props = append(props, &ast.Property{ + Key: key, + Value: val, + Loc: ast.Loc{Start: startLoc.Start, End: endLoc.End}, + }) + + // Optional comma separator + if p.cur.Kind == int(',') { + p.advance() + } + } + + if _, err := p.expect(int(')')); err != nil { + return nil, err + } + + return props, nil +} diff --git a/doris/parser/catalog_test.go b/doris/parser/catalog_test.go new file mode 100644 index 00000000..11394ee2 --- /dev/null +++ b/doris/parser/catalog_test.go @@ -0,0 +1,527 @@ +package parser + +import ( + "testing" + + "github.com/bytebase/omni/doris/ast" +) + +// --------------------------------------------------------------------------- +// CREATE CATALOG +// --------------------------------------------------------------------------- + +func TestCreateCatalog_Basic(t *testing.T) { + file, errs := Parse(`CREATE CATALOG hive_catalog PROPERTIES("type"="hms", "hive.metastore.uris"="thrift://127.0.0.1:7004")`) + 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.CreateCatalogStmt) + if !ok { + t.Fatalf("expected *ast.CreateCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "hive_catalog" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive_catalog") + } + if stmt.External { + t.Error("External should be false") + } + if stmt.IfNotExists { + t.Error("IfNotExists should be false") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "type" { + t.Errorf("Properties[0].Key = %q, want %q", stmt.Properties[0].Key, "type") + } + if stmt.Properties[0].Value != "hms" { + t.Errorf("Properties[0].Value = %q, want %q", stmt.Properties[0].Value, "hms") + } +} + +func TestCreateCatalog_External(t *testing.T) { + file, errs := Parse(`CREATE EXTERNAL CATALOG iceberg_catalog PROPERTIES("type"="iceberg")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.CreateCatalogStmt) + if !ok { + t.Fatalf("expected *ast.CreateCatalogStmt, got %T", file.Stmts[0]) + } + if !stmt.External { + t.Error("External should be true") + } + if stmt.Name != "iceberg_catalog" { + t.Errorf("Name = %q, want %q", stmt.Name, "iceberg_catalog") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "type" || stmt.Properties[0].Value != "iceberg" { + t.Errorf("unexpected property: key=%q val=%q", stmt.Properties[0].Key, stmt.Properties[0].Value) + } +} + +func TestCreateCatalog_IfNotExists(t *testing.T) { + file, errs := Parse(`CREATE CATALOG IF NOT EXISTS my_catalog PROPERTIES("type"="hms")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if !stmt.IfNotExists { + t.Error("IfNotExists should be true") + } + if stmt.Name != "my_catalog" { + t.Errorf("Name = %q, want %q", stmt.Name, "my_catalog") + } +} + +func TestCreateCatalog_WithComment(t *testing.T) { + file, errs := Parse(`CREATE CATALOG hive COMMENT 'hive catalog' PROPERTIES('type'='hms', 'hive.metastore.uris'='thrift://127.0.0.1:7004')`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } + if stmt.Comment != "hive catalog" { + t.Errorf("Comment = %q, want %q", stmt.Comment, "hive catalog") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateCatalog_NoProperties(t *testing.T) { + file, errs := Parse(`CREATE CATALOG my_catalog`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "my_catalog" { + t.Errorf("Name = %q, want %q", stmt.Name, "my_catalog") + } + if len(stmt.Properties) != 0 { + t.Errorf("expected 0 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateCatalog_WithResource(t *testing.T) { + file, errs := Parse(`CREATE CATALOG hms_catalog WITH RESOURCE hms_resource`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "hms_catalog" { + t.Errorf("Name = %q, want %q", stmt.Name, "hms_catalog") + } + if stmt.WithResource != "hms_resource" { + t.Errorf("WithResource = %q, want %q", stmt.WithResource, "hms_resource") + } +} + +func TestCreateCatalog_ES(t *testing.T) { + file, errs := Parse(`CREATE CATALOG es PROPERTIES ("type"="es", "hosts"="http://127.0.0.1:9200")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "es" { + t.Errorf("Name = %q, want %q", stmt.Name, "es") + } + if len(stmt.Properties) != 2 { + t.Fatalf("expected 2 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateCatalog_JDBC(t *testing.T) { + input := `CREATE CATALOG jdbc PROPERTIES ( + "type"="jdbc", + "user"="root", + "password"="123456", + "jdbc_url" = "jdbc:mysql://127.0.0.1:3316/doris_test?useSSL=false", + "driver_url" = "https://example.com/mysql-connector-java-8.0.25.jar", + "driver_class" = "com.mysql.cj.jdbc.Driver" + )` + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "jdbc" { + t.Errorf("Name = %q, want %q", stmt.Name, "jdbc") + } + if len(stmt.Properties) != 6 { + t.Fatalf("expected 6 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateCatalog_Tag(t *testing.T) { + file, errs := Parse(`CREATE CATALOG c PROPERTIES("type"="hms")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_CreateCatalogStmt { + t.Errorf("Tag() = %v, want T_CreateCatalogStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// ALTER CATALOG +// --------------------------------------------------------------------------- + +func TestAlterCatalog_Rename(t *testing.T) { + file, errs := Parse(`ALTER CATALOG ctlg_hive RENAME hive`) + 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.AlterCatalogStmt) + if !ok { + t.Fatalf("expected *ast.AlterCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "ctlg_hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "ctlg_hive") + } + if stmt.Action != ast.AlterCatalogRename { + t.Errorf("Action = %v, want AlterCatalogRename", stmt.Action) + } + if stmt.NewName != "hive" { + t.Errorf("NewName = %q, want %q", stmt.NewName, "hive") + } +} + +func TestAlterCatalog_SetProperties(t *testing.T) { + file, errs := Parse(`ALTER CATALOG hive SET PROPERTIES ('hive.metastore.uris'='thrift://172.21.0.1:9083')`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.AlterCatalogStmt) + if !ok { + t.Fatalf("expected *ast.AlterCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } + if stmt.Action != ast.AlterCatalogSetProperties { + t.Errorf("Action = %v, want AlterCatalogSetProperties", stmt.Action) + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "hive.metastore.uris" { + t.Errorf("Property key = %q, want %q", stmt.Properties[0].Key, "hive.metastore.uris") + } +} + +func TestAlterCatalog_ModifyComment(t *testing.T) { + file, errs := Parse(`ALTER CATALOG hive MODIFY COMMENT "new catalog comment"`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.AlterCatalogStmt) + if !ok { + t.Fatalf("expected *ast.AlterCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } + if stmt.Action != ast.AlterCatalogModifyComment { + t.Errorf("Action = %v, want AlterCatalogModifyComment", stmt.Action) + } + if stmt.Comment != "new catalog comment" { + t.Errorf("Comment = %q, want %q", stmt.Comment, "new catalog comment") + } +} + +func TestAlterCatalog_Property(t *testing.T) { + file, errs := Parse(`ALTER CATALOG c PROPERTY ("key"="value")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.AlterCatalogStmt) + if !ok { + t.Fatalf("expected *ast.AlterCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Action != ast.AlterCatalogSetProperty { + t.Errorf("Action = %v, want AlterCatalogSetProperty", stmt.Action) + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "key" || stmt.Properties[0].Value != "value" { + t.Errorf("unexpected property: key=%q val=%q", stmt.Properties[0].Key, stmt.Properties[0].Value) + } +} + +func TestAlterCatalog_Tag(t *testing.T) { + file, errs := Parse(`ALTER CATALOG c RENAME c2`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_AlterCatalogStmt { + t.Errorf("Tag() = %v, want T_AlterCatalogStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// DROP CATALOG +// --------------------------------------------------------------------------- + +func TestDropCatalog_Basic(t *testing.T) { + file, errs := Parse(`DROP CATALOG hive`) + 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.DropCatalogStmt) + if !ok { + t.Fatalf("expected *ast.DropCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } + if stmt.IfExists { + t.Error("IfExists should be false") + } +} + +func TestDropCatalog_IfExists(t *testing.T) { + file, errs := Parse(`DROP CATALOG IF EXISTS hive`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.DropCatalogStmt) + if !ok { + t.Fatalf("expected *ast.DropCatalogStmt, got %T", file.Stmts[0]) + } + if !stmt.IfExists { + t.Error("IfExists should be true") + } + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } +} + +func TestDropCatalog_Tag(t *testing.T) { + file, errs := Parse(`DROP CATALOG c`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_DropCatalogStmt { + t.Errorf("Tag() = %v, want T_DropCatalogStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// REFRESH CATALOG +// --------------------------------------------------------------------------- + +func TestRefreshCatalog_Basic(t *testing.T) { + file, errs := Parse(`REFRESH CATALOG c`) + 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.RefreshCatalogStmt) + if !ok { + t.Fatalf("expected *ast.RefreshCatalogStmt, got %T", file.Stmts[0]) + } + if stmt.Name != "c" { + t.Errorf("Name = %q, want %q", stmt.Name, "c") + } + if len(stmt.Properties) != 0 { + t.Errorf("expected 0 properties, got %d", len(stmt.Properties)) + } +} + +func TestRefreshCatalog_WithProperties(t *testing.T) { + file, errs := Parse(`REFRESH CATALOG c PROPERTIES("key"="value")`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.RefreshCatalogStmt) + if stmt.Name != "c" { + t.Errorf("Name = %q, want %q", stmt.Name, "c") + } + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "key" || stmt.Properties[0].Value != "value" { + t.Errorf("unexpected property: key=%q val=%q", stmt.Properties[0].Key, stmt.Properties[0].Value) + } +} + +func TestRefreshCatalog_Tag(t *testing.T) { + file, errs := Parse(`REFRESH CATALOG c`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if file.Stmts[0].Tag() != ast.T_RefreshCatalogStmt { + t.Errorf("Tag() = %v, want T_RefreshCatalogStmt", file.Stmts[0].Tag()) + } +} + +// --------------------------------------------------------------------------- +// Legacy corpus — all catalog_create.sql examples +// --------------------------------------------------------------------------- + +func TestCreateCatalog_Legacy_HiveWithHAFS(t *testing.T) { + input := `CREATE CATALOG hive COMMENT 'hive catalog' PROPERTIES ( + 'type'='hms', + 'hive.metastore.uris' = 'thrift://127.0.0.1:7004', + 'dfs.nameservices'='HANN', + 'dfs.ha.namenodes.HANN'='nn1,nn2', + 'dfs.namenode.rpc-address.HANN.nn1'='nn1_host:rpc_port', + 'dfs.namenode.rpc-address.HANN.nn2'='nn2_host:rpc_port', + 'dfs.client.failover.proxy.provider.HANN'='org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider' + )` + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "hive" { + t.Errorf("Name = %q, want %q", stmt.Name, "hive") + } + if stmt.Comment != "hive catalog" { + t.Errorf("Comment = %q, want %q", stmt.Comment, "hive catalog") + } + if len(stmt.Properties) != 7 { + t.Fatalf("expected 7 properties, got %d", len(stmt.Properties)) + } +} + +func TestCreateCatalog_Legacy_JdbcPostgres(t *testing.T) { + input := `CREATE CATALOG jdbc_pg PROPERTIES ( + "type"="jdbc", + "user"="postgres", + "password"="123456", + "jdbc_url" = "jdbc:postgresql://127.0.0.1:5432/demo", + "driver_url" = "file:///path/to/postgresql-42.5.1.jar", + "driver_class" = "org.postgresql.Driver" + )` + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.CreateCatalogStmt) + if stmt.Name != "jdbc_pg" { + t.Errorf("Name = %q, want %q", stmt.Name, "jdbc_pg") + } + if len(stmt.Properties) != 6 { + t.Fatalf("expected 6 properties, got %d", len(stmt.Properties)) + } +} + +// --------------------------------------------------------------------------- +// Legacy corpus — catalog_alter.sql +// --------------------------------------------------------------------------- + +func TestAlterCatalog_Legacy_Rename(t *testing.T) { + file, errs := Parse(`ALTER CATALOG ctlg_hive RENAME hive`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AlterCatalogStmt) + if stmt.Name != "ctlg_hive" || stmt.NewName != "hive" { + t.Errorf("Name=%q NewName=%q", stmt.Name, stmt.NewName) + } +} + +func TestAlterCatalog_Legacy_SetProperties(t *testing.T) { + file, errs := Parse(`ALTER CATALOG hive SET PROPERTIES ('hive.metastore.uris'='thrift://172.21.0.1:9083')`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AlterCatalogStmt) + if stmt.Action != ast.AlterCatalogSetProperties { + t.Errorf("Action = %v, want AlterCatalogSetProperties", stmt.Action) + } +} + +func TestAlterCatalog_Legacy_ModifyComment(t *testing.T) { + file, errs := Parse(`ALTER CATALOG hive MODIFY COMMENT "new catalog comment"`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AlterCatalogStmt) + if stmt.Comment != "new catalog comment" { + t.Errorf("Comment = %q, want %q", stmt.Comment, "new catalog comment") + } +} + +// --------------------------------------------------------------------------- +// Legacy corpus — catalog_drop.sql +// --------------------------------------------------------------------------- + +func TestDropCatalog_Legacy_Basic(t *testing.T) { + file, errs := Parse(`DROP CATALOG hive`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropCatalogStmt) + if stmt.Name != "hive" || stmt.IfExists { + t.Errorf("Name=%q IfExists=%v", stmt.Name, stmt.IfExists) + } +} + +func TestDropCatalog_Legacy_IfExists(t *testing.T) { + file, errs := Parse(`DROP CATALOG IF EXISTS hive`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropCatalogStmt) + if stmt.Name != "hive" || !stmt.IfExists { + t.Errorf("Name=%q IfExists=%v", stmt.Name, stmt.IfExists) + } +} + +// --------------------------------------------------------------------------- +// Multi-statement +// --------------------------------------------------------------------------- + +func TestCatalog_MultiStatement(t *testing.T) { + input := `CREATE CATALOG c1 PROPERTIES("type"="hms"); DROP CATALOG c1` + file, errs := Parse(input) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(file.Stmts) != 2 { + t.Fatalf("expected 2 stmts, got %d", len(file.Stmts)) + } + if _, ok := file.Stmts[0].(*ast.CreateCatalogStmt); !ok { + t.Errorf("Stmts[0]: expected *ast.CreateCatalogStmt, got %T", file.Stmts[0]) + } + if _, ok := file.Stmts[1].(*ast.DropCatalogStmt); !ok { + t.Errorf("Stmts[1]: expected *ast.DropCatalogStmt, got %T", file.Stmts[1]) + } +} + +// --------------------------------------------------------------------------- +// CREATE EXTERNAL TABLE still dispatches correctly (regression) +// --------------------------------------------------------------------------- + +func TestCreateExternalTable_StillWorks(t *testing.T) { + file, errs := Parse(`CREATE EXTERNAL TABLE t (id INT)`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + 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") + } +} diff --git a/doris/parser/parser.go b/doris/parser/parser.go index 1d23e24b..51863050 100644 --- a/doris/parser/parser.go +++ b/doris/parser/parser.go @@ -179,8 +179,17 @@ 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: + // Peek past EXTERNAL to distinguish CREATE EXTERNAL CATALOG from + // CREATE EXTERNAL TABLE (and CREATE EXTERNAL TEMPORARY TABLE, etc.) + if p.peekNext().Kind == kwCATALOG { + return p.parseCreateCatalog() + } + return p.parseCreateTable() + case kwCATALOG: + return p.parseCreateCatalog() case kwVIEW: return p.parseCreateView(createTok.Loc, false) case kwOR: @@ -202,6 +211,8 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseAlterTable() case kwVIEW: return p.parseAlterView() + case kwCATALOG: + return p.parseAlterCatalog() default: return p.unsupported("ALTER") } @@ -214,6 +225,8 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseDropDatabase() case kwVIEW: return p.parseDropView(dropTok.Loc) + case kwCATALOG: + return p.parseDropCatalog() default: return p.unsupported("DROP") } @@ -307,6 +320,11 @@ func (p *Parser) parseStmt() (ast.Node, error) { // Materialized View / Refresh case kwREFRESH: + refreshTok := p.advance() // consume REFRESH; cur is now the object type keyword + if p.cur.Kind == kwCATALOG { + return p.parseRefreshCatalog() + } + _ = refreshTok return p.unsupported("REFRESH") // Job control diff --git a/doris/parser/parser_test.go b/doris/parser/parser_test.go index 25537daa..84cf39dc 100644 --- a/doris/parser/parser_test.go +++ b/doris/parser/parser_test.go @@ -190,7 +190,8 @@ func TestParseAllDispatchCategories(t *testing.T) { {"BACKUP SNAPSHOT s", "BACKUP"}, {"RESTORE SNAPSHOT s", "RESTORE"}, {"RECOVER DATABASE db", "RECOVER"}, - {"REFRESH CATALOG c", "REFRESH"}, + // REFRESH CATALOG is now implemented (T5.2); keep a non-CATALOG form here. + {"REFRESH MATERIALIZED VIEW mv", "REFRESH"}, {"CANCEL LOAD", "CANCEL"}, {"ANALYZE TABLE t", "ANALYZE"}, {"CLEAN ALL PROFILE", "CLEAN"},