Files
MyGoNavi/internal/app/methods_explain_test.go
Syngnat b997788437 feat(explain): 新增 SQL 诊断工作台后端 EXPLAIN 基建
- 数据结构:新增 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 构造与三方言解析器
2026-06-19 12:30:56 +08:00

145 lines
4.9 KiB
Go
Raw Permalink 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 (
"strings"
"testing"
"GoNavi-Wails/internal/connection"
)
// buildExplainQuery 测试:验证各方言生成的 SQL 是否符合预期。
func TestBuildExplainQuery_MySQLUsesFormatJSON(t *testing.T) {
wrapped, post, format, cleanup, err := buildExplainQuery("mysql", "SELECT * FROM t")
if err != nil {
t.Fatalf("mysql 构造失败:%v", err)
}
if want := "EXPLAIN FORMAT=JSON SELECT * FROM t"; wrapped != want {
t.Fatalf("got=%q want=%q", wrapped, want)
}
if len(post) != 0 {
t.Fatalf("mysql 不应有 post 查询got=%v", post)
}
if len(cleanup) != 0 {
t.Fatalf("mysql 不应有 cleanupgot=%v", cleanup)
}
if format != connection.ExplainFormatJSON {
t.Fatalf("format got=%v want=json", format)
}
}
func TestBuildExplainQuery_PostgresUsesAnalyzeBuffersJSON(t *testing.T) {
wrapped, _, format, _, err := buildExplainQuery("postgres", "SELECT * FROM t WHERE id = 1")
if err != nil {
t.Fatalf("postgres 构造失败:%v", err)
}
if !strings.Contains(wrapped, "ANALYZE") || !strings.Contains(wrapped, "BUFFERS") || !strings.Contains(wrapped, "FORMAT JSON") {
t.Fatalf("postgres SQL 应含 ANALYZE BUFFERS FORMAT JSONgot=%q", wrapped)
}
if format != connection.ExplainFormatJSON {
t.Fatalf("format got=%v want=json", format)
}
}
func TestBuildExplainQuery_SQLiteUsesEQP(t *testing.T) {
wrapped, _, format, _, err := buildExplainQuery("sqlite", "SELECT * FROM t")
if err != nil {
t.Fatalf("sqlite 构造失败:%v", err)
}
if want := "EXPLAIN QUERY PLAN SELECT * FROM t"; wrapped != want {
t.Fatalf("got=%q want=%q", wrapped, want)
}
if format != connection.ExplainFormatTable {
t.Fatalf("format got=%v want=table", format)
}
}
func TestBuildExplainQuery_OracleReturnsStatementIDAndCleanup(t *testing.T) {
wrapped, post, _, cleanup, err := buildExplainQuery("oracle", "SELECT * FROM t")
if err != nil {
t.Fatalf("oracle 构造失败:%v", err)
}
if !strings.Contains(wrapped, "EXPLAIN PLAN SET STATEMENT_ID") {
t.Fatalf("oracle 主语句应含 STATEMENT_IDgot=%q", wrapped)
}
if len(post) != 1 || !strings.Contains(post[0], "DBMS_XPLAN.DISPLAY") {
t.Fatalf("oracle post 应含 DBMS_XPLAN.DISPLAYgot=%v", post)
}
if len(cleanup) != 1 || !strings.Contains(cleanup[0], "DELETE FROM plan_table") {
t.Fatalf("oracle cleanup 应含 DELETE FROM plan_tablegot=%v", cleanup)
}
// 验证 statement_id 在三条 SQL 中一致
idInWrapped := extractBetween(wrapped, "STATEMENT_ID = '", "' FOR")
idInPost := extractBetween(post[0], "NULL, '", "'")
idInCleanup := extractBetween(cleanup[0], "statement_id = '", "'")
if idInWrapped == "" || idInWrapped != idInPost || idInWrapped != idInCleanup {
t.Fatalf("statement_id 不一致wrapped=%q post=%q cleanup=%q", idInWrapped, idInPost, idInCleanup)
}
}
func TestBuildExplainQuery_SQLServerSetsShowplanXML(t *testing.T) {
wrapped, post, _, _, err := buildExplainQuery("sqlserver", "SELECT * FROM t")
if err != nil {
t.Fatalf("sqlserver 构造失败:%v", err)
}
if !strings.Contains(wrapped, "SET SHOWPLAN_XML ON") {
t.Fatalf("sqlserver 应 SET SHOWPLAN_XML ONgot=%q", wrapped)
}
if !strings.Contains(wrapped, "SELECT * FROM t") {
t.Fatalf("sqlserver 应保留原 SQLgot=%q", wrapped)
}
if len(post) != 1 || !strings.Contains(post[0], "SET SHOWPLAN_XML OFF") {
t.Fatalf("sqlserver post 应 SET SHOWPLAN_XML OFFgot=%v", post)
}
}
func TestBuildExplainQuery_ClickHouseUsesExplainJSON(t *testing.T) {
wrapped, _, format, _, err := buildExplainQuery("clickhouse", "SELECT * FROM t")
if err != nil {
t.Fatalf("clickhouse 构造失败:%v", err)
}
if want := "EXPLAIN JSON SELECT * FROM t"; wrapped != want {
t.Fatalf("got=%q want=%q", wrapped, want)
}
if format != connection.ExplainFormatJSON {
t.Fatalf("format got=%v want=json", format)
}
}
func TestBuildExplainQuery_PGLikeDialectsSharePath(t *testing.T) {
// gaussdb/opengauss/kingbase/highgo/vastbase 应该复用 PG 的 ANALYZE BUFFERS 路径
for _, dbType := range []string{"gaussdb", "opengauss", "kingbase", "highgo", "vastbase"} {
wrapped, _, format, _, err := buildExplainQuery(dbType, "SELECT 1")
if err != nil {
t.Errorf("%s 构造失败:%v", dbType, err)
continue
}
if !strings.Contains(wrapped, "FORMAT JSON") {
t.Errorf("%s 应使用 FORMAT JSON 路径got=%q", dbType, wrapped)
}
if format != connection.ExplainFormatJSON {
t.Errorf("%s format got=%v want=json", dbType, format)
}
}
}
func TestBuildExplainQuery_UnsupportedDialectReturnsError(t *testing.T) {
_, _, _, _, err := buildExplainQuery("mongodb", "db.t.find()")
if err == nil {
t.Fatal("未支持方言应返回 error")
}
}
// extractBetween 取 s 中 between start 和 end 的第一个匹配子串(测试辅助)。
func extractBetween(s, start, end string) string {
startIdx := strings.Index(s, start)
if startIdx < 0 {
return ""
}
startIdx += len(start)
endIdx := strings.Index(s[startIdx:], end)
if endIdx < 0 {
return ""
}
return s[startIdx : startIdx+endIdx]
}