mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-26 00:11:43 +08:00
- 数据结构:新增 ExplainResult/Node/Stats/IndexSuggestion/DiagnoseReport 等归一化模型,跨方言通用 - 接口扩展:Database 接口新增 ExplainExecer 可选能力,支持驱动自带 EXPLAIN 实现 - 核心入口:DiagnoseQuery 支持 SELECT/WITH 白名单校验、方言调度、原生与 fallback 两条执行路径 - 方言适配:buildExplainQuery 覆盖 MySQL/PostgreSQL/SQLite/Oracle/SQLServer/ClickHouse 7 大主流 - 解析器:MySQL FORMAT=JSON 含表格 fallback、PostgreSQL ANALYZE BUFFERS JSON、SQLite EQP 层级解析 - 测试覆盖:新增 27 个单元测试覆盖 SQL 构造与三方言解析器
139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
package app
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
)
|
||
|
||
// SQLite EQP fixture:单表全表扫描 + filesort。
|
||
const sqliteEQPFullScanWithSort = `id parent notused detail
|
||
2 0 0 SCAN TABLE users
|
||
5 0 0 USE TEMP B-TREE FOR ORDER BY`
|
||
|
||
func TestParseSQLiteExplain_FullScanWithFileSort(t *testing.T) {
|
||
result, err := parseSQLiteExplain("SELECT * FROM users ORDER BY name", sqliteEQPFullScanWithSort, connection.ExplainFormatTable)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
// 2 个独立节点(id 不同,parent 都是 0,无父子关系)
|
||
if len(result.Nodes) != 2 {
|
||
t.Fatalf("应有 2 个节点,got=%d", len(result.Nodes))
|
||
}
|
||
scan := result.Nodes[0]
|
||
if scan.OpType != connection.ExplainOpScan {
|
||
t.Fatalf("第一个应为 SCAN,got=%s", scan.OpType)
|
||
}
|
||
if scan.Table != "users" {
|
||
t.Fatalf("table got=%s want=users", scan.Table)
|
||
}
|
||
if !containsFlag(scan.Flags, connection.ExplainFlagFullScan) {
|
||
t.Fatalf("SCAN 应有 FULL_SCAN flag")
|
||
}
|
||
if result.Stats.HasFullScan != true {
|
||
t.Fatalf("Stats.HasFullScan 应为 true")
|
||
}
|
||
if result.Stats.HasFilesort != true {
|
||
t.Fatalf("Stats.HasFilesort 应为 true")
|
||
}
|
||
}
|
||
|
||
// SQLite EQP fixture:索引扫描。
|
||
const sqliteEQPIndexScan = `id parent notused detail
|
||
3 0 0 SEARCH TABLE users USING INDEX idx_email (email=?)`
|
||
|
||
func TestParseSQLiteExplain_IndexScanExtractsIndex(t *testing.T) {
|
||
result, err := parseSQLiteExplain("SELECT * FROM users WHERE email = 'x'", sqliteEQPIndexScan, connection.ExplainFormatTable)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 1 {
|
||
t.Fatalf("应有 1 个节点,got=%d", len(result.Nodes))
|
||
}
|
||
node := result.Nodes[0]
|
||
if node.OpType != connection.ExplainOpIndexScan {
|
||
t.Fatalf("USING INDEX 应为 INDEX_SCAN,got=%s", node.OpType)
|
||
}
|
||
if node.Table != "users" {
|
||
t.Fatalf("table got=%s want=users", node.Table)
|
||
}
|
||
if node.Index != "idx_email" {
|
||
t.Fatalf("index got=%s want=idx_email", node.Index)
|
||
}
|
||
}
|
||
|
||
// SQLite EQP fixture:主键扫描 + 临时表(distinct)。
|
||
const sqliteEQPPrimaryKeyWithDistinct = `id parent notused detail
|
||
3 0 0 SEARCH TABLE users USING PRIMARY KEY (id=?)
|
||
7 0 0 USE TEMP B-TREE FOR DISTINCT`
|
||
|
||
func TestParseSQLiteExplain_PrimaryKeyAndDistinct(t *testing.T) {
|
||
result, err := parseSQLiteExplain("SELECT DISTINCT name FROM users WHERE id = 1", sqliteEQPPrimaryKeyWithDistinct, connection.ExplainFormatTable)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 2 {
|
||
t.Fatalf("应有 2 个节点,got=%d", len(result.Nodes))
|
||
}
|
||
pk := result.Nodes[0]
|
||
if pk.OpType != connection.ExplainOpIndexScan {
|
||
t.Fatalf("PRIMARY KEY 应为 INDEX_SCAN,got=%s", pk.OpType)
|
||
}
|
||
if pk.Index != "PRIMARY" {
|
||
t.Fatalf("index got=%s want=PRIMARY", pk.Index)
|
||
}
|
||
if result.Stats.HasTempTable != true {
|
||
t.Fatalf("FOR DISTINCT 应触发 TEMP_TABLE flag")
|
||
}
|
||
}
|
||
|
||
// SQLite EQP fixture:父子关系(子查询)。
|
||
const sqliteEQPCorrelatedSubquery = `id parent notused detail
|
||
2 0 0 SCAN TABLE orders
|
||
6 2 0 CORRELATED SCALAR SUBQUERY 1
|
||
8 6 0 SEARCH TABLE users USING INDEX idx_id (id=?)`
|
||
|
||
func TestParseSQLiteExplain_HierarchicalRelationShips(t *testing.T) {
|
||
result, err := parseSQLiteExplain("SELECT *, (SELECT name FROM users WHERE id = o.user_id) FROM orders o", sqliteEQPCorrelatedSubquery, connection.ExplainFormatTable)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if len(result.Nodes) != 3 {
|
||
t.Fatalf("应有 3 个节点,got=%d", len(result.Nodes))
|
||
}
|
||
// orders 是根(parent=0)
|
||
// CORRELATED SCALAR SUBQUERY 的 parent=2 → orders
|
||
// SEARCH 的 parent=6 → subquery
|
||
if result.Nodes[0].ParentID != "" {
|
||
t.Fatalf("根节点 ParentID 应为空,got=%q", result.Nodes[0].ParentID)
|
||
}
|
||
if result.Nodes[1].ParentID != result.Nodes[0].ID {
|
||
t.Fatalf("subquery 节点的 ParentID 应指向 orders")
|
||
}
|
||
if result.Nodes[2].ParentID != result.Nodes[1].ID {
|
||
t.Fatalf("SEARCH 节点的 ParentID 应指向 subquery")
|
||
}
|
||
if len(result.Edges) != 2 {
|
||
t.Fatalf("应有 2 条边,got=%d", len(result.Edges))
|
||
}
|
||
}
|
||
|
||
func TestParseSQLiteExplain_CoveringIndex(t *testing.T) {
|
||
raw := `id parent notused detail
|
||
3 0 0 SEARCH TABLE users USING COVERING INDEX idx_name_email (name=?)`
|
||
result, err := parseSQLiteExplain("SELECT name FROM users WHERE name = 'x'", raw, connection.ExplainFormatTable)
|
||
if err != nil {
|
||
t.Fatalf("解析失败:%v", err)
|
||
}
|
||
if result.Nodes[0].OpType != connection.ExplainOpIndexOnly {
|
||
t.Fatalf("COVERING INDEX 应为 INDEX_ONLY,got=%s", result.Nodes[0].OpType)
|
||
}
|
||
}
|
||
|
||
func TestParseSQLiteExplain_MissingColumnsReturnsError(t *testing.T) {
|
||
_, err := parseSQLiteExplain("SELECT 1", "id parent\n1 0", connection.ExplainFormatTable)
|
||
if err == nil {
|
||
t.Fatal("缺少 detail 列应返回 error")
|
||
}
|
||
}
|