mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-25 16:04:02 +08:00
- 方言解析:新增 Oracle DBMS_XPLAN 表格、SQLServer SHOWPLAN_XML、ClickHouse EXPLAIN JSON 解析器 - 规则引擎:新增 10 条跨方言规则(全表扫描、缺索引 JOIN、filesort、估算偏差、缓冲命中、Nested Loop 高扇出等) - 入口接入:DiagnoseQuery 返回的 Suggestions 自动填充规则匹配结果 - 容错增强:SQLServer strip 默认命名空间与 XML 声明;Oracle 表格列与独立 Predicate 段双源融合 - 测试覆盖:新增 27 个用例覆盖三方言解析与规则触发场景
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
package app
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
)
|
||
|
||
// ClickHouse EXPLAIN JSON fixture:ReadFromMergeTree + Aggregating。
|
||
const clickHouseExplainJSONScanAndAggregate = `{
|
||
"Plan": {
|
||
"Node Type": "Aggregating",
|
||
"Aggregation": {
|
||
"Keys": ["user_id"],
|
||
"Functions": ["count()"]
|
||
},
|
||
"Joined Plans": [
|
||
{
|
||
"Node Type": "ReadFromMergeTree",
|
||
"ReadType": "Default",
|
||
"Parts": 12,
|
||
"Index Granules": 240,
|
||
"Table": "events",
|
||
"Database": "default"
|
||
}
|
||
]
|
||
}
|
||
}`
|
||
|
||
func TestParseClickHouseExplain_ScanAndAggregate(t *testing.T) {
|
||
result, err := parseClickHouseExplain("SELECT user_id, count() FROM events GROUP BY user_id", clickHouseExplainJSONScanAndAggregate, connection.ExplainFormatJSON)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 2 {
|
||
t.Fatalf("应有 2 个节点(Aggregating + ReadFromMergeTree),got=%d", len(result.Nodes))
|
||
}
|
||
aggNode := result.Nodes[0]
|
||
if aggNode.OpType != connection.ExplainOpAggregate {
|
||
t.Fatalf("Aggregating 应为 AGGREGATE,got=%s", aggNode.OpType)
|
||
}
|
||
if !containsFlag(aggNode.Flags, connection.ExplainFlagTempTable) {
|
||
t.Fatalf("Aggregating 应有 TEMP_TABLE flag")
|
||
}
|
||
scanNode := result.Nodes[1]
|
||
if scanNode.OpType != connection.ExplainOpScan {
|
||
t.Fatalf("ReadFromMergeTree 应为 SCAN,got=%s", scanNode.OpType)
|
||
}
|
||
if scanNode.Table != "default.events" {
|
||
t.Fatalf("Table got=%s want=default.events", scanNode.Table)
|
||
}
|
||
// EstRows = Index Granules × 8192 = 240 × 8192 = 1966080
|
||
if scanNode.EstRows != 240*8192 {
|
||
t.Fatalf("EstRows got=%d want=%d", scanNode.EstRows, 240*8192)
|
||
}
|
||
if !containsFlag(scanNode.Flags, connection.ExplainFlagFullScan) {
|
||
t.Fatalf("ReadType=Default 的 MergeTree 应有 FULL_SCAN flag")
|
||
}
|
||
if !containsFlag(scanNode.Flags, connection.ExplainFlagNoIndex) {
|
||
t.Fatalf("ReadType=Default 的 MergeTree 应有 NO_INDEX flag")
|
||
}
|
||
// Edges:Aggregating -> ReadFromMergeTree
|
||
if len(result.Edges) != 1 || result.Edges[0].From != aggNode.ID || result.Edges[0].To != scanNode.ID {
|
||
t.Fatalf("应有 1 条边连接 AGGREGATE -> SCAN")
|
||
}
|
||
}
|
||
|
||
func TestParseClickHouseExplain_IndexedReadNoFullScanFlag(t *testing.T) {
|
||
raw := `{
|
||
"Plan": {
|
||
"Node Type": "ReadFromMergeTree",
|
||
"ReadType": "InReverseOrder",
|
||
"Parts": 5,
|
||
"Index Granules": 30,
|
||
"Table": "t",
|
||
"Database": "default"
|
||
}
|
||
}`
|
||
result, err := parseClickHouseExplain("SELECT * FROM default.t ORDER BY id DESC", raw, connection.ExplainFormatJSON)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 1 {
|
||
t.Fatalf("应有 1 个节点,got=%d", len(result.Nodes))
|
||
}
|
||
node := result.Nodes[0]
|
||
// ReadType 不是 Default,不应触发 FULL_SCAN
|
||
if containsFlag(node.Flags, connection.ExplainFlagFullScan) {
|
||
t.Fatalf("ReadType=InReverseOrder 不应是 FULL_SCAN")
|
||
}
|
||
}
|
||
|
||
func TestParseClickHouseExplain_ArrayForm(t *testing.T) {
|
||
raw := `[
|
||
{
|
||
"Plan": {
|
||
"Node Type": "Limit",
|
||
"Joined Plans": [
|
||
{"Node Type": "ReadFromMergeTree", "ReadType": "Default", "Parts": 1, "Index Granules": 10, "Table": "t"}
|
||
]
|
||
}
|
||
}
|
||
]`
|
||
result, err := parseClickHouseExplain("SELECT * FROM t LIMIT 10", raw, connection.ExplainFormatJSON)
|
||
if err != nil {
|
||
t.Fatalf("数组形式解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 2 {
|
||
t.Fatalf("应有 2 个节点(Limit + ReadFromMergeTree),got=%d", len(result.Nodes))
|
||
}
|
||
if result.Nodes[0].OpType != connection.ExplainOpLimit {
|
||
t.Fatalf("Limit 节点应为 LIMIT,got=%s", result.Nodes[0].OpType)
|
||
}
|
||
}
|
||
|
||
func TestParseClickHouseExplain_InvalidJSONReturnsError(t *testing.T) {
|
||
_, err := parseClickHouseExplain("SELECT 1", "{ not valid json", connection.ExplainFormatJSON)
|
||
if err == nil {
|
||
t.Fatal("非法 JSON 应返回 error")
|
||
}
|
||
}
|
||
|
||
func TestParseClickHouseExplain_EmptyReturnsError(t *testing.T) {
|
||
_, err := parseClickHouseExplain("SELECT 1", " ", connection.ExplainFormatJSON)
|
||
if err == nil {
|
||
t.Fatal("空输入应返回 error")
|
||
}
|
||
}
|