package app
import (
"encoding/xml"
"fmt"
"strings"
"GoNavi-Wails/internal/connection"
)
// SQL Server SHOWPLAN_XML 解析。
//
// 典型 XML 结构(简化):
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// ...
//
//
//
//
//
//
//
//
//
//
//
// 解析要点:
// - RelOp 是核心节点(递归嵌套),每个含 PhysicalOp + LogicalOp + EstimateRows + EstimatedTotalSubtreeCost
// - 嵌套在 RelOp 内的同级 RelOp(在 IndexScan/NestedLoops/Hash 等子元素下)是子节点
// - PhysicalOp 直接对应执行算子(Clustered Index Scan / Index Seek / Hash Match / Sort / ...)
// - Object 子元素含 Table/Index 信息
// - Predicate 的 ScalarOperator ScalarString 含过滤条件(供规则引擎提取列名)
// - RunTimeCountersPerThread 含 ActualRows(对应 ANALYZE 信息)
// sqlServerShowPlanXML 是 SHOWPLAN_XML 顶层文档的 Go 结构(部分字段,未识别的留 raw)。
type sqlServerShowPlanXML struct {
XMLName xml.Name `xml:"ShowPlanXML"`
Batches []sqlServerXMLBatch `xml:"BatchSequence>Batch"`
}
type sqlServerXMLBatch struct {
Statements []sqlServerXMLStmtSimple `xml:"Statements>StmtSimple"`
}
type sqlServerXMLStmtSimple struct {
StatementText string `xml:"StatementText,attr"`
QueryPlan *sqlServerXMLPlan `xml:"QueryPlan"`
}
type sqlServerXMLPlan struct {
RelOps []sqlServerXMLRelOp `xml:"RelOp"`
}
// sqlServerXMLRelOp 是 SHOWPLAN_XML 的核心节点;嵌套子 RelOp 通过多种容器元素持有。
// 为简化解析:先把所有层级的 RelOp 平铺出来(按 NodeId 排序),再按 NodeId 父子推断。
type sqlServerXMLRelOp struct {
NodeID int `xml:"NodeId,attr"`
PhysicalOp string `xml:"PhysicalOp,attr"`
LogicalOp string `xml:"LogicalOp,attr"`
EstimateRows float64 `xml:"EstimateRows,attr"`
EstimateIO float64 `xml:"EstimateIO,attr"`
EstimateCPU float64 `xml:"EstimateCPU,attr"`
EstimatedTotalSubtreeCost float64 `xml:"EstimatedTotalSubtreeCost,attr"`
EstimateRebinds float64 `xml:"EstimateRebinds,attr"`
Parallel int `xml:"Parallel,attr"`
// 子节点容器(不同 PhysicalOp 对应不同容器名,这里全收)
Objects []sqlServerXMLObject `xml:"Object"`
IndexScan *sqlServerXMLContainer `xml:"IndexScan"`
NestedLoops *sqlServerXMLContainer `xml:"NestedLoops"`
Hash *sqlServerXMLContainer `xml:"Hash"`
Merge *sqlServerXMLContainer `xml:"Merge"`
Concat *sqlServerXMLContainer `xml:"Concat"`
Sort *sqlServerXMLContainer `xml:"Sort"`
Filter *sqlServerXMLContainer `xml:"Filter"`
ComputeScalar *sqlServerXMLContainer `xml:"ComputeScalar"`
Top *sqlServerXMLContainer `xml:"Top"`
GenericRelOps []sqlServerXMLContainer `xml:",any"` // 兜底:未识别容器
Predicate *sqlServerXMLPredicate `xml:"Predicate"`
RunTimeInfo *sqlServerXMLRunTimeInfo `xml:"RunTimeInformation"`
}
type sqlServerXMLObject struct {
Database string `xml:"Database,attr"`
Schema string `xml:"Schema,attr"`
Table string `xml:"Table,attr"`
Index string `xml:"Index,attr"`
}
type sqlServerXMLContainer struct {
Objects []sqlServerXMLObject `xml:"Object"`
RelOps []sqlServerXMLRelOp `xml:"RelOp"`
Predicate *sqlServerXMLPredicate `xml:"Predicate"`
}
type sqlServerXMLPredicate struct {
ScalarString string `xml:"ScalarOperator>ScalarString"`
}
type sqlServerXMLRunTimeInfo struct {
RunTimeCounters []sqlServerXMLRunTimeCounter `xml:"RunTimeCountersPerThread"`
}
type sqlServerXMLRunTimeCounter struct {
ActualRows int64 `xml:"ActualRows,attr"`
ActualElapsedMs float64 `xml:"ActualElapsedms,attr"`
ActualScans int64 `xml:"ActualScans,attr"`
}
func parseSQLServerExplain(sourceSQL, raw string, format connection.ExplainFormat) (connection.ExplainResult, error) {
result := connection.ExplainResult{
DBType: "sqlserver",
SourceSQL: sourceSQL,
}
resetExplainNodeID()
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return result, fmt.Errorf("SHOWPLAN_XML 输出为空")
}
// SQL Server 输出的 XML 含 `` 声明,Go encoding/xml 不支持 utf-16
// 直接报 "encoding declared but not supported"。从 标签开始截取即可规避。
showPlanStart := strings.Index(trimmed, " 0 {
obj := rel.Objects[0]
node.Table = stripSQLServerBrackets(obj.Table)
node.Index = stripSQLServerBrackets(obj.Index)
}
// Predicate
if rel.Predicate != nil && rel.Predicate.ScalarString != "" {
if node.Extra == nil {
node.Extra = map[string]any{}
}
node.Extra["filter"] = rel.Predicate.ScalarString
}
// ActualRows(来自 RunTimeCountersPerThread 累加)
if rel.RunTimeInfo != nil {
var actualRows int64
var elapsedMs float64
for _, c := range rel.RunTimeInfo.RunTimeCounters {
actualRows += c.ActualRows
elapsedMs += c.ActualElapsedMs
}
node.ActualRows = actualRows
node.DurationMs = elapsedMs
}
// 物理算子归类
switch node.OpType {
case connection.ExplainOpScan:
node.Flags = append(node.Flags, connection.ExplainFlagFullScan, connection.ExplainFlagNoIndex)
case connection.ExplainOpSort:
node.Flags = append(node.Flags, connection.ExplainFlagFilesort)
case connection.ExplainOpAggregate, connection.ExplainOpMaterialize:
node.Flags = append(node.Flags, connection.ExplainFlagTempTable)
}
// 关联 LogicalOp 优化提示
if strings.Contains(strings.ToLower(rel.LogicalOp), "aggregate") {
node.Flags = append(node.Flags, connection.ExplainFlagTempTable)
}
nodeID := appendExplainChild(result, parentID, node)
nodeByOpID[rel.NodeID] = nodeID
// 递归所有可能的容器
containers := []*sqlServerXMLContainer{
rel.IndexScan, rel.NestedLoops, rel.Hash, rel.Merge,
rel.Concat, rel.Sort, rel.Filter, rel.ComputeScalar, rel.Top,
}
for _, c := range containers {
if c == nil {
continue
}
for i := range c.RelOps {
parseSQLServerRelOp(&c.RelOps[i], nodeID, result, nodeByOpID)
}
}
// GenericRelOps 是 ,any 兜底容器,按需递归
for i := range rel.GenericRelOps {
for j := range rel.GenericRelOps[i].RelOps {
parseSQLServerRelOp(&rel.GenericRelOps[i].RelOps[j], nodeID, result, nodeByOpID)
}
}
}
// classifySQLServerPhysicalOp 把 SQLServer PhysicalOp/LogicalOp 归一化到通用 OpType。
// 参考官方文档:Clustered Index Scan / Index Seek / RID Lookup / Key Lookup / Hash Match / Nested Loops /
// Merge Join / Sort / Stream Aggregate / Filter / Compute Scalar / Top / Spool / Table-valued function。
func classifySQLServerPhysicalOp(physical, logical string) string {
p := strings.ToLower(strings.TrimSpace(physical))
l := strings.ToLower(strings.TrimSpace(logical))
switch {
case strings.Contains(p, "index scan"), strings.Contains(p, "clustered index scan"), strings.Contains(p, "table scan"):
return connection.ExplainOpScan
case strings.Contains(p, "index seek"), strings.Contains(p, "clustered index seek"):
return connection.ExplainOpIndexScan
case strings.Contains(p, "key lookup"), strings.Contains(p, "rid lookup"):
return connection.ExplainOpIndexScan
case strings.Contains(p, "hash match"), strings.Contains(p, "nested loops"), strings.Contains(p, "merge join"):
return connection.ExplainOpJoin
case strings.Contains(p, "sort"):
return connection.ExplainOpSort
case strings.Contains(l, "aggregate"), strings.Contains(p, "stream aggregate"), strings.Contains(p, "hash match") && strings.Contains(l, "aggregate"):
return connection.ExplainOpAggregate
case strings.Contains(p, "filter"):
return connection.ExplainOpFilter
case strings.Contains(p, "top"):
return connection.ExplainOpLimit
case strings.Contains(p, "spool"):
return connection.ExplainOpMaterialize
case strings.Contains(p, "compute scalar"):
return connection.ExplainOpOther
default:
return connection.ExplainOpOther
}
}
// stripSQLServerBrackets 去掉 SQLServer 标识符的方括号:[users] → users。
func stripSQLServerBrackets(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "[")
s = strings.TrimSuffix(s, "]")
return s
}