diff --git a/doris/ast/loc.go b/doris/ast/loc.go index 5a74bbd5..a6bab845 100644 --- a/doris/ast/loc.go +++ b/doris/ast/loc.go @@ -340,6 +340,22 @@ func NodeLoc(n Node) Loc { return v.Loc case *ShowJobTaskStmt: return v.Loc + case *AddConstraintStmt: + return v.Loc + case *DropConstraintStmt: + return v.Loc + case *ShowConstraintsStmt: + return v.Loc + case *AnalyzeStmt: + return v.Loc + case *ShowAnalyzeStmt: + return v.Loc + case *ShowStatsStmt: + return v.Loc + case *DropStatsStmt: + return v.Loc + case *KillAnalyzeStmt: + return v.Loc default: return NoLoc() } diff --git a/doris/ast/nodetags.go b/doris/ast/nodetags.go index 1d941eca..dfcf7aba 100644 --- a/doris/ast/nodetags.go +++ b/doris/ast/nodetags.go @@ -552,6 +552,31 @@ const ( // T_ShowJobTaskStmt is the tag for *ShowJobTaskStmt. T_ShowJobTaskStmt + // Statistics / Analyze / Constraint management nodes (T8.3). + + // T_AddConstraintStmt is the tag for *AddConstraintStmt. + T_AddConstraintStmt + + // T_DropConstraintStmt is the tag for *DropConstraintStmt. + T_DropConstraintStmt + + // T_ShowConstraintsStmt is the tag for *ShowConstraintsStmt. + T_ShowConstraintsStmt + + // T_AnalyzeStmt is the tag for *AnalyzeStmt. + T_AnalyzeStmt + + // T_ShowAnalyzeStmt is the tag for *ShowAnalyzeStmt. + T_ShowAnalyzeStmt + + // T_ShowStatsStmt is the tag for *ShowStatsStmt. + T_ShowStatsStmt + + // T_DropStatsStmt is the tag for *DropStatsStmt. + T_DropStatsStmt + + // T_KillAnalyzeStmt is the tag for *KillAnalyzeStmt. + T_KillAnalyzeStmt ) // String returns a human-readable representation of the tag. @@ -887,6 +912,22 @@ func (t NodeTag) String() string { return "ShowJobStmt" case T_ShowJobTaskStmt: return "ShowJobTaskStmt" + case T_AddConstraintStmt: + return "AddConstraintStmt" + case T_DropConstraintStmt: + return "DropConstraintStmt" + case T_ShowConstraintsStmt: + return "ShowConstraintsStmt" + case T_AnalyzeStmt: + return "AnalyzeStmt" + case T_ShowAnalyzeStmt: + return "ShowAnalyzeStmt" + case T_ShowStatsStmt: + return "ShowStatsStmt" + case T_DropStatsStmt: + return "DropStatsStmt" + case T_KillAnalyzeStmt: + return "KillAnalyzeStmt" default: return "Unknown" } diff --git a/doris/ast/stats_nodes.go b/doris/ast/stats_nodes.go new file mode 100644 index 00000000..b08cc6ad --- /dev/null +++ b/doris/ast/stats_nodes.go @@ -0,0 +1,162 @@ +package ast + +// This file holds AST node types for statistics, analyze, and constraint +// management statements (T8.3). + +// --------------------------------------------------------------------------- +// ADD / DROP CONSTRAINT +// --------------------------------------------------------------------------- + +// AddConstraintStmt represents: +// +// ALTER TABLE name ADD CONSTRAINT cname PRIMARY KEY (cols) +// ALTER TABLE name ADD CONSTRAINT cname UNIQUE (cols) +// ALTER TABLE name ADD CONSTRAINT cname FOREIGN KEY (cols) REFERENCES ref_table (cols) +type AddConstraintStmt struct { + Table *ObjectName + Name string // constraint name + Type string // "PRIMARY KEY", "UNIQUE", "FOREIGN KEY" + Columns []string + RefTable *ObjectName // for FOREIGN KEY + RefColumns []string // for FOREIGN KEY + Loc Loc +} + +// Tag implements Node. +func (n *AddConstraintStmt) Tag() NodeTag { return T_AddConstraintStmt } + +var _ Node = (*AddConstraintStmt)(nil) + +// DropConstraintStmt represents: +// +// ALTER TABLE name DROP CONSTRAINT cname +type DropConstraintStmt struct { + Table *ObjectName + Name string + Loc Loc +} + +// Tag implements Node. +func (n *DropConstraintStmt) Tag() NodeTag { return T_DropConstraintStmt } + +var _ Node = (*DropConstraintStmt)(nil) + +// ShowConstraintsStmt represents: +// +// SHOW CONSTRAINTS FROM table +type ShowConstraintsStmt struct { + Table *ObjectName + Loc Loc +} + +// Tag implements Node. +func (n *ShowConstraintsStmt) Tag() NodeTag { return T_ShowConstraintsStmt } + +var _ Node = (*ShowConstraintsStmt)(nil) + +// --------------------------------------------------------------------------- +// ANALYZE +// --------------------------------------------------------------------------- + +// AnalyzeStmt represents: +// +// ANALYZE DATABASE name +// ANALYZE TABLE name [(col1, col2, ...)] +// ANALYZE PROFILE +// +// followed by optional modifiers: +// +// WITH SAMPLE PERCENT n +// WITH SAMPLE ROWS n +// WITH SYNC +// WITH INCREMENTAL +// PROPERTIES(...) +type AnalyzeStmt struct { + TargetType string // "DATABASE", "TABLE", "PROFILE", or "" + Target *ObjectName // nil for PROFILE + Columns []string // ANALYZE TABLE t(col1, col2) — optional column list + Properties []*Property // WITH ... or PROPERTIES(...) + Loc Loc +} + +// Tag implements Node. +func (n *AnalyzeStmt) Tag() NodeTag { return T_AnalyzeStmt } + +var _ Node = (*AnalyzeStmt)(nil) + +// --------------------------------------------------------------------------- +// SHOW ANALYZE / SHOW STATS +// --------------------------------------------------------------------------- + +// ShowAnalyzeStmt represents: +// +// SHOW [ALL | QUEUED] ANALYZE [JOB] [job_id | FOR table | LIKE pat] +// SHOW ANALYZE TASK STATUS job_id +type ShowAnalyzeStmt struct { + All bool // SHOW ALL ANALYZE + Queued bool // SHOW QUEUED ANALYZE JOBS + IsTask bool // SHOW ANALYZE TASK STATUS + JobID int64 // numeric job id (0 if absent) + For *ObjectName // FOR table + Like string // LIKE 'pat' + Where Node // WHERE expr + Loc Loc +} + +// Tag implements Node. +func (n *ShowAnalyzeStmt) Tag() NodeTag { return T_ShowAnalyzeStmt } + +var _ Node = (*ShowAnalyzeStmt)(nil) + +// ShowStatsStmt represents: +// +// SHOW [COLUMN | TABLE | INDEX | PARTITION] STATS [target] [args...] +type ShowStatsStmt struct { + Type string // "COLUMN", "TABLE", "INDEX", "PARTITION", or "" + Target *ObjectName // optional table reference + Where Node // optional WHERE clause + Loc Loc +} + +// Tag implements Node. +func (n *ShowStatsStmt) Tag() NodeTag { return T_ShowStatsStmt } + +var _ Node = (*ShowStatsStmt)(nil) + +// --------------------------------------------------------------------------- +// DROP STATS +// --------------------------------------------------------------------------- + +// DropStatsStmt represents: +// +// DROP STATS target [(col1, col2)] +// DROP EXPIRED STATS target +// DROP CACHED STATS target +type DropStatsStmt struct { + Variant string // "", "EXPIRED", "CACHED" + Target *ObjectName + Columns []string // optional column list for plain DROP STATS + Loc Loc +} + +// Tag implements Node. +func (n *DropStatsStmt) Tag() NodeTag { return T_DropStatsStmt } + +var _ Node = (*DropStatsStmt)(nil) + +// --------------------------------------------------------------------------- +// KILL ANALYZE +// --------------------------------------------------------------------------- + +// KillAnalyzeStmt represents: +// +// KILL ANALYZE job_id +type KillAnalyzeStmt struct { + JobID int64 + Loc Loc +} + +// Tag implements Node. +func (n *KillAnalyzeStmt) Tag() NodeTag { return T_KillAnalyzeStmt } + +var _ Node = (*KillAnalyzeStmt)(nil) diff --git a/doris/ast/walk_children.go b/doris/ast/walk_children.go index 54fbeb19..13fc7d6c 100644 --- a/doris/ast/walk_children.go +++ b/doris/ast/walk_children.go @@ -860,5 +860,49 @@ func walkChildren(v Visitor, node Node) { if n.For != nil { Walk(v, n.For) } + + // Statistics / Analyze / Constraint management nodes (T8.3). + case *AddConstraintStmt: + if n.Table != nil { + Walk(v, n.Table) + } + if n.RefTable != nil { + Walk(v, n.RefTable) + } + case *DropConstraintStmt: + if n.Table != nil { + Walk(v, n.Table) + } + case *ShowConstraintsStmt: + if n.Table != nil { + Walk(v, n.Table) + } + case *AnalyzeStmt: + if n.Target != nil { + Walk(v, n.Target) + } + for _, prop := range n.Properties { + Walk(v, prop) + } + case *ShowAnalyzeStmt: + if n.For != nil { + Walk(v, n.For) + } + if n.Where != nil { + Walk(v, n.Where) + } + case *ShowStatsStmt: + if n.Target != nil { + Walk(v, n.Target) + } + if n.Where != nil { + Walk(v, n.Where) + } + case *DropStatsStmt: + if n.Target != nil { + Walk(v, n.Target) + } + case *KillAnalyzeStmt: + // leaf node, no children } } diff --git a/doris/parser/alter_table.go b/doris/parser/alter_table.go index 22bfa9e8..d6edc429 100644 --- a/doris/parser/alter_table.go +++ b/doris/parser/alter_table.go @@ -26,6 +26,17 @@ func (p *Parser) parseAlterTable() (ast.Node, error) { } stmt.Name = name + // ALTER TABLE name ADD CONSTRAINT ... — produce AddConstraintStmt directly. + if p.cur.Kind == kwADD && p.peekNext().Kind == kwCONSTRAINT { + p.advance() // consume ADD + return p.parseAddConstraint(startLoc, name) + } + + // ALTER TABLE name DROP CONSTRAINT ... — produce DropConstraintStmt directly. + if p.cur.Kind == kwDROP && p.peekNext().Kind == kwCONSTRAINT { + return p.parseDropConstraint(startLoc, name) + } + // Parse comma-separated action list. // Most actions start with a keyword (ADD, DROP, MODIFY, RENAME, SET, ...). // Exception: after DROP ROLLUP name, a bare identifier means another rollup diff --git a/doris/parser/parser.go b/doris/parser/parser.go index 9e5152b6..d0d3f487 100644 --- a/doris/parser/parser.go +++ b/doris/parser/parser.go @@ -350,6 +350,21 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseDropUser(dropTok.Loc) case kwJOB: return p.parseDropJob(dropTok.Loc) + case kwSTATS: + p.advance() // consume STATS + return p.parseDropStats(dropTok.Loc, "") + case kwEXPIRED: + p.advance() // consume EXPIRED + if _, err := p.expect(kwSTATS); err != nil { + return nil, err + } + return p.parseDropStats(dropTok.Loc, "EXPIRED") + case kwCACHED: + p.advance() // consume CACHED + if _, err := p.expect(kwSTATS); err != nil { + return nil, err + } + return p.parseDropStats(dropTok.Loc, "CACHED") default: return p.unsupported("DROP") } @@ -412,6 +427,8 @@ func (p *Parser) parseStmt() (ast.Node, error) { } // Return unsupported, but we already consumed SHOW so we need to emit the error at cur. return p.unsupported("SHOW") + p.advance() // consume SHOW; dispatch inside parseShow + return p.parseShow() case kwDESCRIBE: return p.parseDescribe() case kwDESC: @@ -450,6 +467,9 @@ func (p *Parser) parseStmt() (ast.Node, error) { return p.parseAdminStmt(adminTok.Loc) case kwKILL: killTok := p.advance() // consume KILL + if p.cur.Kind == kwANALYZE { + return p.parseKillAnalyze(killTok.Loc) + } return p.parseKill(killTok.Loc) case kwLOCK: lockTok := p.advance() // consume LOCK @@ -549,7 +569,8 @@ func (p *Parser) parseStmt() (ast.Node, error) { // Analyze / Sync / Warm case kwANALYZE: - return p.unsupported("ANALYZE") + p.advance() // consume ANALYZE + return p.parseAnalyze() case kwSYNC: syncTok := p.advance() // consume SYNC return p.parseSyncStmt(syncTok.Loc) diff --git a/doris/parser/parser_test.go b/doris/parser/parser_test.go index 6942740d..672e5ef6 100644 --- a/doris/parser/parser_test.go +++ b/doris/parser/parser_test.go @@ -166,10 +166,9 @@ func TestParseAllDispatchCategories(t *testing.T) { wantMsg string }{ {"DROP TABLE t", "DROP"}, - // Most statement categories are now implemented; only a few REFRESH - // targets and ANALYZE (until T8.3 merges) remain in the unsupported list. + // Most statement categories are now implemented; only REFRESH (with + // unrecognized target kinds) remains as an example of unsupported dispatch. {"REFRESH DATABASE db", "REFRESH"}, - {"ANALYZE TABLE t", "ANALYZE"}, } for _, tt := range tests { _, errs := Parse(tt.input) diff --git a/doris/parser/show.go b/doris/parser/show.go index b735d3de..ebb42400 100644 --- a/doris/parser/show.go +++ b/doris/parser/show.go @@ -50,6 +50,39 @@ func (p *Parser) parseShow() (ast.Node, error) { return p.parseShowJob(startLoc) } + // SHOW ANALYZE / SHOW STATS / SHOW CONSTRAINTS (T8.3) — special-case routing + if p.cur.Kind == kwANALYZE { + all := stmt.Args == "ALL" + return p.parseShowAnalyze(startLoc, all, false) + } + if p.cur.Kind == kwQUEUED && p.peekNext().Kind == kwANALYZE { + p.advance() // consume QUEUED + return p.parseShowAnalyze(startLoc, false, true) + } + if p.cur.Kind == kwCONSTRAINTS { + return p.parseShowConstraints(startLoc) + } + if p.cur.Kind == kwSTATS { + return p.parseShowStats(startLoc, "") + } + // SHOW COLUMN/INDEX/PARTITION/TABLE STATS + if p.peekNext().Kind == kwSTATS { + switch p.cur.Kind { + case kwCOLUMN: + p.advance() + return p.parseShowStats(startLoc, "COLUMN") + case kwINDEX: + p.advance() + return p.parseShowStats(startLoc, "INDEX") + case kwPARTITION: + p.advance() + return p.parseShowStats(startLoc, "PARTITION") + case kwTABLE: + p.advance() + return p.parseShowStats(startLoc, "TABLE") + } + } + // SHOW [ALL] ROUTINE LOAD (T6.2) — special-case routing if p.cur.Kind == kwROUTINE { // SHOW ALL ROUTINE LOAD case: the ALL was already consumed and stored in stmt.Args. diff --git a/doris/parser/stats.go b/doris/parser/stats.go new file mode 100644 index 00000000..772d1a04 --- /dev/null +++ b/doris/parser/stats.go @@ -0,0 +1,545 @@ +package parser + +import ( + "strings" + + "github.com/bytebase/omni/doris/ast" +) + +// --------------------------------------------------------------------------- +// ANALYZE statement +// --------------------------------------------------------------------------- + +// parseAnalyze parses: +// +// ANALYZE DATABASE name +// ANALYZE TABLE name [(col1, col2, ...)] +// ANALYZE PROFILE +// +// followed by optional modifiers: +// +// WITH SAMPLE PERCENT n +// WITH SAMPLE ROWS n +// WITH SYNC +// WITH INCREMENTAL +// PROPERTIES(...) +// +// The ANALYZE keyword has already been consumed; cur is the next token. +func (p *Parser) parseAnalyze() (ast.Node, error) { + startLoc := p.prev.Loc + + stmt := &ast.AnalyzeStmt{} + endLoc := startLoc + + switch p.cur.Kind { + case kwDATABASE, kwSCHEMA: + p.advance() // consume DATABASE/SCHEMA + stmt.TargetType = "DATABASE" + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Target = target + endLoc = target.Loc + + case kwTABLE: + p.advance() // consume TABLE + stmt.TargetType = "TABLE" + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Target = target + endLoc = target.Loc + + // Optional column list: (col1, col2, ...) + if p.cur.Kind == int('(') { + cols, colsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + endLoc = colsEnd + } + + case kwPROFILE: + p.advance() // consume PROFILE + stmt.TargetType = "PROFILE" + endLoc = p.prev.Loc + + default: + // Bare ANALYZE with no target type — treat as ANALYZE TABLE (Doris also + // allows ANALYZE name directly for table-level analyze). + if isIdentifierToken(p.cur.Kind) { + stmt.TargetType = "TABLE" + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Target = target + endLoc = target.Loc + + // Optional column list + if p.cur.Kind == int('(') { + cols, colsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + endLoc = colsEnd + } + } + } + + // Optional WITH modifiers and PROPERTIES + for { + if p.cur.Kind == kwWITH { + p.advance() // consume WITH + prop, propEnd, err := p.parseAnalyzeWith() + if err != nil { + return nil, err + } + stmt.Properties = append(stmt.Properties, prop) + endLoc = propEnd + } else if p.cur.Kind == kwPROPERTIES { + props, err := p.parseProperties() + if err != nil { + return nil, err + } + stmt.Properties = append(stmt.Properties, props...) + if len(props) > 0 { + endLoc = ast.NodeLoc(props[len(props)-1]) + } + } else { + break + } + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseAnalyzeWith parses a single WITH modifier after ANALYZE: +// +// WITH SAMPLE PERCENT n +// WITH SAMPLE ROWS n +// WITH SYNC +// WITH INCREMENTAL +// +// WITH has already been consumed; cur is the modifier keyword. +// Returns a synthetic Property{Key: modifier, Value: n} for value-bearing +// modifiers or Property{Key: modifier, Value: ""} for flags. +func (p *Parser) parseAnalyzeWith() (*ast.Property, ast.Loc, error) { + startLoc := p.cur.Loc + + switch p.cur.Kind { + case kwSAMPLE: + p.advance() // consume SAMPLE + switch p.cur.Kind { + case kwPERCENT: + p.advance() // consume PERCENT + if p.cur.Kind != tokInt && p.cur.Kind != tokFloat { + return nil, ast.Loc{}, p.syntaxErrorAtCur() + } + val := p.cur.Str + endLoc := p.cur.Loc + p.advance() + return &ast.Property{Key: "SAMPLE PERCENT", Value: val, Loc: startLoc.Merge(endLoc)}, endLoc, nil + case kwROWS: + p.advance() // consume ROWS + if p.cur.Kind != tokInt { + return nil, ast.Loc{}, p.syntaxErrorAtCur() + } + val := p.cur.Str + endLoc := p.cur.Loc + p.advance() + return &ast.Property{Key: "SAMPLE ROWS", Value: val, Loc: startLoc.Merge(endLoc)}, endLoc, nil + default: + return nil, ast.Loc{}, p.syntaxErrorAtCur() + } + + case kwSYNC: + endLoc := p.cur.Loc + p.advance() + return &ast.Property{Key: "SYNC", Value: "", Loc: startLoc.Merge(endLoc)}, endLoc, nil + + case kwINCREMENTAL: + endLoc := p.cur.Loc + p.advance() + return &ast.Property{Key: "INCREMENTAL", Value: "", Loc: startLoc.Merge(endLoc)}, endLoc, nil + + default: + // Unknown WITH modifier — consume as raw identifier to avoid hard failure + if isIdentifierToken(p.cur.Kind) { + key := strings.ToUpper(p.cur.Str) + endLoc := p.cur.Loc + p.advance() + return &ast.Property{Key: key, Value: "", Loc: startLoc.Merge(endLoc)}, endLoc, nil + } + return nil, ast.Loc{}, p.syntaxErrorAtCur() + } +} + +// --------------------------------------------------------------------------- +// SHOW variants: SHOW ANALYZE, SHOW STATS, SHOW CONSTRAINTS +// --------------------------------------------------------------------------- + +// parseShowAnalyze parses SHOW [ALL|QUEUED] ANALYZE [TASK STATUS job_id | JOB? ...] +// +// The SHOW keyword has already been consumed. cur may be ALL, QUEUED, or +// ANALYZE directly. +func (p *Parser) parseShowAnalyze(startLoc ast.Loc, all, queued bool) (ast.Node, error) { + // cur is ANALYZE — consume it. + p.advance() + + stmt := &ast.ShowAnalyzeStmt{ + All: all, + Queued: queued, + } + endLoc := p.prev.Loc + + // SHOW ANALYZE TASK STATUS job_id + if p.cur.Kind == kwTASK { + p.advance() // consume TASK + // Consume optional STATUS keyword + if p.cur.Kind == kwSTATUS { + p.advance() + } + stmt.IsTask = true + if p.cur.Kind == tokInt { + stmt.JobID = p.cur.Ival + endLoc = p.cur.Loc + p.advance() + } + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil + } + + // Optional JOB keyword (ignored, consumed) + if p.cur.Kind == kwJOB { + p.advance() + } + + // Optional: job_id (integer), FOR table, bare table name, or LIKE pattern + switch p.cur.Kind { + case tokInt: + stmt.JobID = p.cur.Ival + endLoc = p.cur.Loc + p.advance() + + case kwFOR: + p.advance() // consume FOR + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.For = target + endLoc = target.Loc + + case kwLIKE: + p.advance() // consume LIKE + if p.cur.Kind != tokString { + return nil, p.syntaxErrorAtCur() + } + stmt.Like = p.cur.Str + endLoc = p.cur.Loc + p.advance() + + default: + // Bare identifier used as table target (e.g., SHOW ANALYZE test1 WHERE ...) + if isIdentifierToken(p.cur.Kind) { + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.For = target + endLoc = target.Loc + } + } + + // Optional WHERE clause + if p.cur.Kind == kwWHERE { + p.advance() // consume WHERE + where, err := p.parseExpr() + if err != nil { + return nil, err + } + stmt.Where = where + endLoc = ast.NodeLoc(where) + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseShowStats parses SHOW [COLUMN|TABLE|INDEX|PARTITION] STATS [target] [WHERE...] +// +// The SHOW keyword has already been consumed. The optional type keyword (COLUMN, +// TABLE, etc.) may or may not be present; the STATS keyword must follow. +// statsType is the already-consumed type qualifier (empty string if absent). +func (p *Parser) parseShowStats(startLoc ast.Loc, statsType string) (ast.Node, error) { + // cur is STATS — consume it + p.advance() + + stmt := &ast.ShowStatsStmt{ + Type: statsType, + } + endLoc := p.prev.Loc + + // Optional target name + if isIdentifierToken(p.cur.Kind) { + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Target = target + endLoc = target.Loc + } + + // Optional WHERE clause + if p.cur.Kind == kwWHERE { + p.advance() // consume WHERE + where, err := p.parseExpr() + if err != nil { + return nil, err + } + stmt.Where = where + endLoc = ast.NodeLoc(where) + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseShowConstraints parses SHOW CONSTRAINTS FROM table +// +// SHOW has already been consumed; cur is CONSTRAINTS. +func (p *Parser) parseShowConstraints(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume CONSTRAINTS + + stmt := &ast.ShowConstraintsStmt{} + endLoc := p.prev.Loc + + // Expect FROM table + if p.cur.Kind == kwFROM || p.cur.Kind == kwIN { + p.advance() // consume FROM / IN + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Table = target + endLoc = target.Loc + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseShow is the top-level SHOW dispatcher. +// +// (Note: parseShow is defined in show.go (T7.3); analyze/stats/constraints +// routing is wired into that function. The standalone parseShow here was +// removed during the T8.3 rebase onto main.) + +// --------------------------------------------------------------------------- +// DROP STATS / DROP EXPIRED STATS / DROP CACHED STATS +// --------------------------------------------------------------------------- + +// parseDropStats parses: +// +// DROP STATS target [(col1, col2)] +// DROP EXPIRED STATS target +// DROP CACHED STATS target +// +// DROP has already been consumed; variant is "" | "EXPIRED" | "CACHED". +// The STATS keyword has also been consumed by the caller for EXPIRED/CACHED. +// For plain DROP STATS, the caller consumed STATS; target is next. +func (p *Parser) parseDropStats(startLoc ast.Loc, variant string) (ast.Node, error) { + stmt := &ast.DropStatsStmt{Variant: variant} + endLoc := startLoc + + // target table + target, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.Target = target + endLoc = target.Loc + + // Optional column list for plain DROP STATS + if variant == "" && p.cur.Kind == int('(') { + cols, colsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + endLoc = colsEnd + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// KILL ANALYZE +// --------------------------------------------------------------------------- + +// parseKillAnalyze parses KILL ANALYZE job_id. +// +// KILL has already been consumed; cur is ANALYZE. +func (p *Parser) parseKillAnalyze(startLoc ast.Loc) (ast.Node, error) { + p.advance() // consume ANALYZE + + stmt := &ast.KillAnalyzeStmt{} + endLoc := p.prev.Loc + + if p.cur.Kind == tokInt { + stmt.JobID = p.cur.Ival + endLoc = p.cur.Loc + p.advance() + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// ALTER TABLE ADD/DROP CONSTRAINT +// --------------------------------------------------------------------------- + +// parseAddConstraint parses: +// +// ALTER TABLE name ADD CONSTRAINT cname PRIMARY KEY (cols) +// ALTER TABLE name ADD CONSTRAINT cname UNIQUE (cols) +// ALTER TABLE name ADD CONSTRAINT cname FOREIGN KEY (cols) REFERENCES ref (cols) +// +// ALTER TABLE and the table name have been parsed; table is the table ObjectName. +// ADD has already been consumed; cur is CONSTRAINT. +func (p *Parser) parseAddConstraint(startLoc ast.Loc, table *ast.ObjectName) (ast.Node, error) { + p.advance() // consume CONSTRAINT + + stmt := &ast.AddConstraintStmt{Table: table} + endLoc := p.prev.Loc + + // constraint name + name, nameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + endLoc = nameLoc + + switch p.cur.Kind { + case kwPRIMARY: + p.advance() // consume PRIMARY + if _, err := p.expect(kwKEY); err != nil { + return nil, err + } + stmt.Type = "PRIMARY KEY" + cols, colsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + endLoc = colsEnd + + case kwUNIQUE: + p.advance() // consume UNIQUE + stmt.Type = "UNIQUE" + cols, colsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + endLoc = colsEnd + + case kwFOREIGN: + p.advance() // consume FOREIGN + if _, err := p.expect(kwKEY); err != nil { + return nil, err + } + stmt.Type = "FOREIGN KEY" + cols, _, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.Columns = cols + + if _, err := p.expect(kwREFERENCES); err != nil { + return nil, err + } + refTable, err := p.parseMultipartIdentifier() + if err != nil { + return nil, err + } + stmt.RefTable = refTable + endLoc = refTable.Loc + + if p.cur.Kind == int('(') { + refCols, refColsEnd, err := p.parseParenIdentifierList() + if err != nil { + return nil, err + } + stmt.RefColumns = refCols + endLoc = refColsEnd + } + + default: + return nil, p.syntaxErrorAtCur() + } + + stmt.Loc = startLoc.Merge(endLoc) + return stmt, nil +} + +// parseDropConstraint parses: +// +// ALTER TABLE name DROP CONSTRAINT cname +// +// DROP and CONSTRAINT have not yet been consumed; cur is DROP. +// table is the already-parsed table ObjectName. +func (p *Parser) parseDropConstraint(startLoc ast.Loc, table *ast.ObjectName) (ast.Node, error) { + p.advance() // consume DROP + p.advance() // consume CONSTRAINT + + stmt := &ast.DropConstraintStmt{Table: table} + + name, nameLoc, err := p.parseIdentifier() + if err != nil { + return nil, err + } + stmt.Name = name + stmt.Loc = startLoc.Merge(nameLoc) + return stmt, nil +} + +// --------------------------------------------------------------------------- +// Helper: parseParenIdentifierList — parses (id1, id2, ...) returning bare names +// --------------------------------------------------------------------------- + +// parseParenIdentifierList parses a parenthesised comma-separated list of +// identifiers: (col1, col2, ...). +// Returns the names and the Loc of the closing ')'. +func (p *Parser) parseParenIdentifierList() ([]string, ast.Loc, error) { + if _, err := p.expect(int('(')); err != nil { + return nil, ast.Loc{}, err + } + + var names []string + for p.cur.Kind != int(')') && p.cur.Kind != tokEOF { + name, _, err := p.parseIdentifier() + if err != nil { + return nil, ast.Loc{}, err + } + names = append(names, name) + if p.cur.Kind == int(',') { + p.advance() + } + } + + closeTok, err := p.expect(int(')')) + if err != nil { + return nil, ast.Loc{}, err + } + return names, closeTok.Loc, nil +} diff --git a/doris/parser/stats_test.go b/doris/parser/stats_test.go new file mode 100644 index 00000000..8035de87 --- /dev/null +++ b/doris/parser/stats_test.go @@ -0,0 +1,638 @@ +package parser + +import ( + "testing" + + "github.com/bytebase/omni/doris/ast" +) + +// --------------------------------------------------------------------------- +// ANALYZE +// --------------------------------------------------------------------------- + +func TestAnalyze_Table(t *testing.T) { + file, errs := Parse("ANALYZE TABLE lineitem") + 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.AnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.AnalyzeStmt, got %T", file.Stmts[0]) + } + if stmt.TargetType != "TABLE" { + t.Errorf("TargetType = %q, want TABLE", stmt.TargetType) + } + if stmt.Target == nil || stmt.Target.String() != "lineitem" { + t.Errorf("Target = %v, want lineitem", stmt.Target) + } + if len(stmt.Columns) != 0 { + t.Errorf("expected no columns, got %v", stmt.Columns) + } +} + +func TestAnalyze_TableWithSamplePercent(t *testing.T) { + file, errs := Parse("ANALYZE TABLE lineitem WITH SAMPLE PERCENT 10") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "SAMPLE PERCENT" { + t.Errorf("property key = %q, want SAMPLE PERCENT", stmt.Properties[0].Key) + } + if stmt.Properties[0].Value != "10" { + t.Errorf("property value = %q, want 10", stmt.Properties[0].Value) + } +} + +func TestAnalyze_TableWithSampleRows(t *testing.T) { + file, errs := Parse("ANALYZE TABLE lineitem WITH SAMPLE ROWS 100000") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if len(stmt.Properties) != 1 { + t.Fatalf("expected 1 property, got %d", len(stmt.Properties)) + } + if stmt.Properties[0].Key != "SAMPLE ROWS" { + t.Errorf("property key = %q, want SAMPLE ROWS", stmt.Properties[0].Key) + } + if stmt.Properties[0].Value != "100000" { + t.Errorf("property value = %q, want 100000", stmt.Properties[0].Value) + } +} + +func TestAnalyze_TableWithSync(t *testing.T) { + file, errs := Parse("ANALYZE TABLE t WITH SYNC") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if len(stmt.Properties) != 1 || stmt.Properties[0].Key != "SYNC" { + t.Errorf("expected SYNC property, got %v", stmt.Properties) + } +} + +func TestAnalyze_TableWithIncremental(t *testing.T) { + file, errs := Parse("ANALYZE TABLE t WITH INCREMENTAL") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if len(stmt.Properties) != 1 || stmt.Properties[0].Key != "INCREMENTAL" { + t.Errorf("expected INCREMENTAL property, got %v", stmt.Properties) + } +} + +func TestAnalyze_TableWithColumns(t *testing.T) { + file, errs := Parse("ANALYZE TABLE t (col1, col2)") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if len(stmt.Columns) != 2 { + t.Fatalf("expected 2 columns, got %d", len(stmt.Columns)) + } + if stmt.Columns[0] != "col1" || stmt.Columns[1] != "col2" { + t.Errorf("columns = %v, want [col1 col2]", stmt.Columns) + } +} + +func TestAnalyze_Database(t *testing.T) { + file, errs := Parse("ANALYZE DATABASE mydb") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if stmt.TargetType != "DATABASE" { + t.Errorf("TargetType = %q, want DATABASE", stmt.TargetType) + } + if stmt.Target.String() != "mydb" { + t.Errorf("Target = %v, want mydb", stmt.Target) + } +} + +func TestAnalyze_QualifiedTable(t *testing.T) { + file, errs := Parse("ANALYZE TABLE mydb.lineitem WITH SAMPLE PERCENT 10") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if stmt.Target.String() != "mydb.lineitem" { + t.Errorf("Target = %v, want mydb.lineitem", stmt.Target) + } +} + +// --------------------------------------------------------------------------- +// SHOW ANALYZE +// --------------------------------------------------------------------------- + +func TestShowAnalyze_Basic(t *testing.T) { + file, errs := Parse("SHOW ANALYZE test1 WHERE STATE=\"FINISHED\"") + 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.ShowAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.ShowAnalyzeStmt, got %T", file.Stmts[0]) + } + if stmt.For == nil || stmt.For.String() != "test1" { + t.Errorf("For = %v, want test1", stmt.For) + } + if stmt.Where == nil { + t.Error("expected WHERE clause") + } +} + +func TestShowAnalyze_ByJobID(t *testing.T) { + file, errs := Parse("SHOW ANALYZE 1738725887903") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.ShowAnalyzeStmt, got %T", file.Stmts[0]) + } + if stmt.JobID != 1738725887903 { + t.Errorf("JobID = %d, want 1738725887903", stmt.JobID) + } +} + +func TestShowAnalyze_All(t *testing.T) { + file, errs := Parse("SHOW ALL ANALYZE") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.ShowAnalyzeStmt, got %T", file.Stmts[0]) + } + if !stmt.All { + t.Error("expected All = true") + } +} + +func TestShowAnalyze_Queued(t *testing.T) { + file, errs := Parse("SHOW QUEUED ANALYZE") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.ShowAnalyzeStmt, got %T", file.Stmts[0]) + } + if !stmt.Queued { + t.Error("expected Queued = true") + } +} + +func TestShowAnalyze_TaskStatus(t *testing.T) { + file, errs := Parse("SHOW ANALYZE TASK STATUS 12345") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.ShowAnalyzeStmt, got %T", file.Stmts[0]) + } + if !stmt.IsTask { + t.Error("expected IsTask = true") + } + if stmt.JobID != 12345 { + t.Errorf("JobID = %d, want 12345", stmt.JobID) + } +} + +func TestShowAnalyze_ForTable(t *testing.T) { + file, errs := Parse("SHOW ANALYZE FOR mydb.t1") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if stmt.For == nil || stmt.For.String() != "mydb.t1" { + t.Errorf("For = %v, want mydb.t1", stmt.For) + } +} + +// --------------------------------------------------------------------------- +// SHOW STATS +// --------------------------------------------------------------------------- + +func TestShowTableStats(t *testing.T) { + file, errs := Parse("SHOW TABLE STATS test1") + 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.ShowStatsStmt) + if !ok { + t.Fatalf("expected *ast.ShowStatsStmt, got %T", file.Stmts[0]) + } + if stmt.Type != "TABLE" { + t.Errorf("Type = %q, want TABLE", stmt.Type) + } + if stmt.Target == nil || stmt.Target.String() != "test1" { + t.Errorf("Target = %v, want test1", stmt.Target) + } +} + +func TestShowColumnStats(t *testing.T) { + file, errs := Parse("SHOW COLUMN STATS mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowStatsStmt) + if stmt.Type != "COLUMN" { + t.Errorf("Type = %q, want COLUMN", stmt.Type) + } +} + +func TestShowStats_NoType(t *testing.T) { + file, errs := Parse("SHOW STATS mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowStatsStmt) + if stmt.Type != "" { + t.Errorf("Type = %q, want empty", stmt.Type) + } + if stmt.Target == nil || stmt.Target.String() != "mytable" { + t.Errorf("Target = %v, want mytable", stmt.Target) + } +} + +// --------------------------------------------------------------------------- +// SHOW CONSTRAINTS +// --------------------------------------------------------------------------- + +func TestShowConstraints_FromTable(t *testing.T) { + file, errs := Parse("SHOW CONSTRAINTS FROM mytable") + 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.ShowConstraintsStmt) + if !ok { + t.Fatalf("expected *ast.ShowConstraintsStmt, got %T", file.Stmts[0]) + } + if stmt.Table == nil || stmt.Table.String() != "mytable" { + t.Errorf("Table = %v, want mytable", stmt.Table) + } +} + +func TestShowConstraints_QualifiedTable(t *testing.T) { + file, errs := Parse("SHOW CONSTRAINTS FROM mydb.mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowConstraintsStmt) + if stmt.Table.String() != "mydb.mytable" { + t.Errorf("Table = %v, want mydb.mytable", stmt.Table) + } +} + +// --------------------------------------------------------------------------- +// DROP STATS +// --------------------------------------------------------------------------- + +func TestDropStats_Table(t *testing.T) { + file, errs := Parse("DROP STATS table1") + 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.DropStatsStmt) + if !ok { + t.Fatalf("expected *ast.DropStatsStmt, got %T", file.Stmts[0]) + } + if stmt.Variant != "" { + t.Errorf("Variant = %q, want empty", stmt.Variant) + } + if stmt.Target == nil || stmt.Target.String() != "table1" { + t.Errorf("Target = %v, want table1", stmt.Target) + } + if len(stmt.Columns) != 0 { + t.Errorf("expected no columns, got %v", stmt.Columns) + } +} + +func TestDropStats_WithColumns(t *testing.T) { + file, errs := Parse("DROP STATS table1 (col1, col2)") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropStatsStmt) + if len(stmt.Columns) != 2 { + t.Fatalf("expected 2 columns, got %d", len(stmt.Columns)) + } + if stmt.Columns[0] != "col1" || stmt.Columns[1] != "col2" { + t.Errorf("columns = %v, want [col1 col2]", stmt.Columns) + } +} + +func TestDropStats_Expired(t *testing.T) { + file, errs := Parse("DROP EXPIRED STATS mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.DropStatsStmt) + if !ok { + t.Fatalf("expected *ast.DropStatsStmt, got %T", file.Stmts[0]) + } + if stmt.Variant != "EXPIRED" { + t.Errorf("Variant = %q, want EXPIRED", stmt.Variant) + } +} + +func TestDropStats_Cached(t *testing.T) { + file, errs := Parse("DROP CACHED STATS mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.DropStatsStmt) + if !ok { + t.Fatalf("expected *ast.DropStatsStmt, got %T", file.Stmts[0]) + } + if stmt.Variant != "CACHED" { + t.Errorf("Variant = %q, want CACHED", stmt.Variant) + } + if stmt.Target.String() != "mytable" { + t.Errorf("Target = %v, want mytable", stmt.Target) + } +} + +// --------------------------------------------------------------------------- +// KILL ANALYZE +// --------------------------------------------------------------------------- + +func TestKillAnalyze(t *testing.T) { + file, errs := Parse("KILL ANALYZE 12345") + 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.KillAnalyzeStmt) + if !ok { + t.Fatalf("expected *ast.KillAnalyzeStmt, got %T", file.Stmts[0]) + } + if stmt.JobID != 12345 { + t.Errorf("JobID = %d, want 12345", stmt.JobID) + } +} + +// --------------------------------------------------------------------------- +// ADD / DROP CONSTRAINT +// --------------------------------------------------------------------------- + +func TestAlterTableAddConstraintPrimaryKey(t *testing.T) { + file, errs := Parse("ALTER TABLE t ADD CONSTRAINT pk_id PRIMARY KEY (id)") + 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.AddConstraintStmt) + if !ok { + t.Fatalf("expected *ast.AddConstraintStmt, got %T", file.Stmts[0]) + } + if stmt.Table == nil || stmt.Table.String() != "t" { + t.Errorf("Table = %v, want t", stmt.Table) + } + if stmt.Name != "pk_id" { + t.Errorf("Name = %q, want pk_id", stmt.Name) + } + if stmt.Type != "PRIMARY KEY" { + t.Errorf("Type = %q, want PRIMARY KEY", stmt.Type) + } + if len(stmt.Columns) != 1 || stmt.Columns[0] != "id" { + t.Errorf("Columns = %v, want [id]", stmt.Columns) + } +} + +func TestAlterTableAddConstraintUnique(t *testing.T) { + file, errs := Parse("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.AddConstraintStmt) + if !ok { + t.Fatalf("expected *ast.AddConstraintStmt, got %T", file.Stmts[0]) + } + if stmt.Type != "UNIQUE" { + t.Errorf("Type = %q, want UNIQUE", stmt.Type) + } + if len(stmt.Columns) != 1 || stmt.Columns[0] != "email" { + t.Errorf("Columns = %v, want [email]", stmt.Columns) + } +} + +func TestAlterTableAddConstraintForeignKey(t *testing.T) { + file, errs := Parse("ALTER TABLE orders ADD CONSTRAINT fk_cust FOREIGN KEY (customer_id) REFERENCES customers (id)") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt, ok := file.Stmts[0].(*ast.AddConstraintStmt) + if !ok { + t.Fatalf("expected *ast.AddConstraintStmt, got %T", file.Stmts[0]) + } + if stmt.Type != "FOREIGN KEY" { + t.Errorf("Type = %q, want FOREIGN KEY", stmt.Type) + } + if len(stmt.Columns) != 1 || stmt.Columns[0] != "customer_id" { + t.Errorf("Columns = %v, want [customer_id]", stmt.Columns) + } + if stmt.RefTable == nil || stmt.RefTable.String() != "customers" { + t.Errorf("RefTable = %v, want customers", stmt.RefTable) + } + if len(stmt.RefColumns) != 1 || stmt.RefColumns[0] != "id" { + t.Errorf("RefColumns = %v, want [id]", stmt.RefColumns) + } +} + +func TestAlterTableDropConstraint(t *testing.T) { + file, errs := Parse("ALTER TABLE t DROP CONSTRAINT pk_id") + 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.DropConstraintStmt) + if !ok { + t.Fatalf("expected *ast.DropConstraintStmt, got %T", file.Stmts[0]) + } + if stmt.Table == nil || stmt.Table.String() != "t" { + t.Errorf("Table = %v, want t", stmt.Table) + } + if stmt.Name != "pk_id" { + t.Errorf("Name = %q, want pk_id", stmt.Name) + } +} + +// --------------------------------------------------------------------------- +// Legacy corpus forms (from statistics.sql) +// --------------------------------------------------------------------------- + +func TestLegacyCorpus_AnalyzeWithSamplePercent(t *testing.T) { + // ANALYZE TABLE lineitem WITH SAMPLE PERCENT 10; + file, errs := Parse("ANALYZE TABLE lineitem WITH SAMPLE PERCENT 10") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if stmt.Target.String() != "lineitem" { + t.Errorf("Target = %v, want lineitem", stmt.Target) + } + if stmt.Properties[0].Key != "SAMPLE PERCENT" || stmt.Properties[0].Value != "10" { + t.Errorf("property = %v", stmt.Properties[0]) + } +} + +func TestLegacyCorpus_AnalyzeWithSampleRows(t *testing.T) { + // ANALYZE TABLE lineitem WITH SAMPLE ROWS 100000; + file, errs := Parse("ANALYZE TABLE lineitem WITH SAMPLE ROWS 100000") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if stmt.Properties[0].Key != "SAMPLE ROWS" || stmt.Properties[0].Value != "100000" { + t.Errorf("property = %v", stmt.Properties[0]) + } +} + +func TestLegacyCorpus_DropStats(t *testing.T) { + // DROP STATS table1; + file, errs := Parse("DROP STATS table1") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropStatsStmt) + if stmt.Target.String() != "table1" { + t.Errorf("Target = %v, want table1", stmt.Target) + } +} + +func TestLegacyCorpus_DropStatsWithColumns(t *testing.T) { + // DROP STATS table1 (col1, col2); + file, errs := Parse("DROP STATS table1 (col1, col2)") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropStatsStmt) + if len(stmt.Columns) != 2 { + t.Fatalf("expected 2 columns, got %d", len(stmt.Columns)) + } +} + +func TestLegacyCorpus_ShowTableStats(t *testing.T) { + // SHOW TABLE STATS test1; + file, errs := Parse("SHOW TABLE STATS test1") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowStatsStmt) + if stmt.Type != "TABLE" || stmt.Target.String() != "test1" { + t.Errorf("Type=%q Target=%v", stmt.Type, stmt.Target) + } +} + +func TestLegacyCorpus_ShowAnalyzeWithWhere(t *testing.T) { + // SHOW ANALYZE test1 WHERE STATE="FINISHED"; + file, errs := Parse(`SHOW ANALYZE test1 WHERE STATE="FINISHED"`) + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if stmt.For == nil || stmt.For.String() != "test1" { + t.Errorf("For = %v, want test1", stmt.For) + } + if stmt.Where == nil { + t.Error("expected WHERE clause") + } +} + +func TestLegacyCorpus_ShowAnalyzeByJobID(t *testing.T) { + // SHOW ANALYZE 1738725887903 + file, errs := Parse("SHOW ANALYZE 1738725887903") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.ShowAnalyzeStmt) + if stmt.JobID != 1738725887903 { + t.Errorf("JobID = %d, want 1738725887903", stmt.JobID) + } +} + +// --------------------------------------------------------------------------- +// Location tracking sanity +// --------------------------------------------------------------------------- + +func TestAnalyze_LocIsValid(t *testing.T) { + file, errs := Parse("ANALYZE TABLE t") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.AnalyzeStmt) + if !stmt.Loc.IsValid() { + t.Error("expected valid Loc") + } +} + +func TestDropStats_LocIsValid(t *testing.T) { + file, errs := Parse("DROP STATS mytable") + if len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + stmt := file.Stmts[0].(*ast.DropStatsStmt) + if !stmt.Loc.IsValid() { + t.Error("expected valid Loc") + } +} + +func TestNodeTagsForStatsNodes(t *testing.T) { + tests := []struct { + input string + tag ast.NodeTag + }{ + {"ANALYZE TABLE t", ast.T_AnalyzeStmt}, + {"SHOW ANALYZE", ast.T_ShowAnalyzeStmt}, + {"SHOW TABLE STATS t", ast.T_ShowStatsStmt}, + {"SHOW CONSTRAINTS FROM t", ast.T_ShowConstraintsStmt}, + {"DROP STATS t", ast.T_DropStatsStmt}, + {"KILL ANALYZE 1", ast.T_KillAnalyzeStmt}, + {"ALTER TABLE t ADD CONSTRAINT c PRIMARY KEY (id)", ast.T_AddConstraintStmt}, + {"ALTER TABLE t DROP CONSTRAINT c", ast.T_DropConstraintStmt}, + } + + for _, tt := range tests { + file, errs := Parse(tt.input) + if len(errs) != 0 { + t.Errorf("%q: unexpected errors: %v", tt.input, errs) + continue + } + if len(file.Stmts) != 1 { + t.Errorf("%q: expected 1 stmt, got %d", tt.input, len(file.Stmts)) + continue + } + if got := file.Stmts[0].Tag(); got != tt.tag { + t.Errorf("%q: Tag() = %v, want %v", tt.input, got, tt.tag) + } + } +}