Files
MyGoNavi/internal/app/explain_parse_oracle_test.go
Syngnat 8e24e40fdd feat(explain): 补齐 Oracle/SQLServer/ClickHouse 解析器与索引建议规则引擎
- 方言解析:新增 Oracle DBMS_XPLAN 表格、SQLServer SHOWPLAN_XML、ClickHouse EXPLAIN JSON 解析器
- 规则引擎:新增 10 条跨方言规则(全表扫描、缺索引 JOIN、filesort、估算偏差、缓冲命中、Nested Loop 高扇出等)
- 入口接入:DiagnoseQuery 返回的 Suggestions 自动填充规则匹配结果
- 容错增强:SQLServer strip 默认命名空间与 XML 声明;Oracle 表格列与独立 Predicate 段双源融合
- 测试覆盖:新增 27 个用例覆盖三方言解析与规则触发场景
2026-06-19 12:45:15 +08:00

127 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"testing"
"GoNavi-Wails/internal/connection"
)
// Oracle DBMS_XPLAN.DISPLAY fixture含主表 + Predicate + 多段落。
const oracleXPlanOutput = `Plan hash value: 1234567890
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Predicate Information |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 200K| 50 (4)| 00:00:01 | |
|* 1 | TABLE ACCESS FULL| USERS | 10000 | 200K| 50 (4)| 00:00:01 | filter ("AGE">18) |
-----------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$1 / USERS@SEL$1
Column Projection Information (identified by operation id):
-----------------------------------------------------------
1 - "ID"[NUMBER,22], "NAME"[VARCHAR2,100]
`
func TestParseOracleExplain_TableAccessFullWithPredicate(t *testing.T) {
result, err := parseOracleExplain("SELECT * FROM users WHERE age > 18", oracleXPlanOutput, connection.ExplainFormatTable)
if err != nil {
t.Fatalf("解析失败:%v", err)
}
if len(result.Nodes) != 2 {
t.Fatalf("应有 2 个节点SELECT STATEMENT + TABLE ACCESSgot=%d", len(result.Nodes))
}
// 节点 0 是 SELECT STATEMENT节点 1 是 TABLE ACCESS FULL带缩进挂在 0 下)
scan := result.Nodes[1]
if scan.OpType != connection.ExplainOpScan {
t.Fatalf("TABLE ACCESS FULL 应为 SCANgot=%s", scan.OpType)
}
if scan.Table != "USERS" {
t.Fatalf("table got=%s want=USERS", scan.Table)
}
if scan.EstRows != 10000 {
t.Fatalf("EstRows got=%d want=10000", scan.EstRows)
}
if scan.Cost != 50 {
t.Fatalf("Cost got=%v want=50", scan.Cost)
}
if !containsFlag(scan.Flags, connection.ExplainFlagFullScan) {
t.Fatalf("TABLE ACCESS FULL 应有 FULL_SCAN flag")
}
if scan.Extra["filter"] != `filter ("AGE">18)` {
t.Fatalf("Predicate 应附加到 Extra.filtergot=%v", scan.Extra["filter"])
}
// SELECT STATEMENT 是父节点
if len(result.Edges) != 1 || result.Edges[0].To != scan.ID {
t.Fatalf("应有 1 条边指向 TABLE ACCESS 节点")
}
}
const oracleXPlanHashJoin = `Plan hash value: 9876543210
-------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost | Predicate Info |
-------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000 | 200 | |
| 1 | HASH JOIN | | 1000 | 200 | |
| 2 | TABLE ACCESS FULL| USERS | 100 | 10 | |
| 3 | INDEX RANGE SCAN| ORD_IX | 50000 | 20 |access("UID" = 1) |
-------------------------------------------------------------------------
`
func TestParseOracleExplain_HashJoinWithNestedChildren(t *testing.T) {
result, err := parseOracleExplain("SELECT * FROM users u JOIN orders o ON u.id = o.user_id", oracleXPlanHashJoin, connection.ExplainFormatTable)
if err != nil {
t.Fatalf("解析失败:%v", err)
}
if len(result.Nodes) != 4 {
t.Fatalf("应有 4 个节点SELECT + HASH JOIN + 2 子节点got=%d", len(result.Nodes))
}
// HASH JOIN 是 SELECT STATEMENT 的子
// TABLE ACCESS FULL 和 INDEX RANGE SCAN 是 HASH JOIN 的子(缩进更深)
hashJoin := result.Nodes[1]
if hashJoin.OpType != connection.ExplainOpJoin {
t.Fatalf("HASH JOIN 应为 JOINgot=%s", hashJoin.OpType)
}
// 找到 INDEX RANGE SCAN 节点
var indexNode *connection.ExplainNode
for i := range result.Nodes {
if result.Nodes[i].OpType == connection.ExplainOpIndexScan {
indexNode = &result.Nodes[i]
break
}
}
if indexNode == nil {
t.Fatal("应有一个 INDEX RANGE SCAN 节点")
}
if indexNode.Index != "ORD_IX" {
t.Fatalf("Index got=%s want=ORD_IX", indexNode.Index)
}
// Predicate 关联id=3独立 Predicate 段落覆盖了表格列的简短摘要)
if indexNode.Extra["filter"] != `access("UID" = 1)` {
t.Fatalf("Predicate 应附加到 INDEX RANGE SCAN 节点got=%v", indexNode.Extra["filter"])
}
}
func TestParseOracleExplain_EmptyReturnsError(t *testing.T) {
_, err := parseOracleExplain("SELECT 1", " ", connection.ExplainFormatTable)
if err == nil {
t.Fatal("空输入应返回 error")
}
}
func TestParseOracleExplain_NoTableReturnsWarning(t *testing.T) {
result, err := parseOracleExplain("SELECT 1", "Plan hash value: 1\nsome random text", connection.ExplainFormatTable)
if err != nil {
t.Fatalf("无表格的输入应降级返回 warning 而非 error%v", err)
}
if result.RawFormat != connection.ExplainFormatText {
t.Fatalf("RawFormat got=%v want=text", result.RawFormat)
}
if len(result.Warnings) == 0 {
t.Fatal("应有降级 warning")
}
}