mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
- 方言解析:新增 Oracle DBMS_XPLAN 表格、SQLServer SHOWPLAN_XML、ClickHouse EXPLAIN JSON 解析器 - 规则引擎:新增 10 条跨方言规则(全表扫描、缺索引 JOIN、filesort、估算偏差、缓冲命中、Nested Loop 高扇出等) - 入口接入:DiagnoseQuery 返回的 Suggestions 自动填充规则匹配结果 - 容错增强:SQLServer strip 默认命名空间与 XML 声明;Oracle 表格列与独立 Predicate 段双源融合 - 测试覆盖:新增 27 个用例覆盖三方言解析与规则触发场景
127 lines
5.0 KiB
Go
127 lines
5.0 KiB
Go
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 ACCESS),got=%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 应为 SCAN,got=%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.filter,got=%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 应为 JOIN,got=%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")
|
||
}
|
||
}
|