Files
MyGoNavi/internal/app/explain_parse_clickhouse_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

129 lines
3.9 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"
)
// ClickHouse EXPLAIN JSON fixtureReadFromMergeTree + 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 + ReadFromMergeTreegot=%d", len(result.Nodes))
}
aggNode := result.Nodes[0]
if aggNode.OpType != connection.ExplainOpAggregate {
t.Fatalf("Aggregating 应为 AGGREGATEgot=%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 应为 SCANgot=%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")
}
// EdgesAggregating -> 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 + ReadFromMergeTreegot=%d", len(result.Nodes))
}
if result.Nodes[0].OpType != connection.ExplainOpLimit {
t.Fatalf("Limit 节点应为 LIMITgot=%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")
}
}