mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 01:24:12 +08:00
- SQL 导出:formatSQLValue 按方言转义反斜杠,修复 MySQL 系 dump 还原时静默改写数据、 且以反斜杠结尾的值吞掉闭合引号致源库恶意行可执行任意 SQL 的问题;非 MySQL 方言保持原样 - 明文凭据:daily_secrets.json 由 0o644 改为 0o600、目录改 0o700,并对历史文件显式 Chmod - SSH 隧道:RegisterSSHNetwork 改为确定性 network 名并复用缓存客户端,消除驱动全局 dialer 表随重连线性增长、永久钉住 ssh.Client 的连接与 goroutine 泄漏 - xlsx 导入:单元格 r 属性列号增加 OOXML 16384 上限并提前熔断,避免篡改文件放大分配致 OOM - 数据导出:三处非 SQL 导出入口捕获 file.Close 错误,不再在落盘失败时返回“导出成功” - 数据根迁移:copyFile 捕获 Close 错误并补 Sync,避免被截断的副本被判定为迁移成功 - 执行计划:节点 ID 改为按 ExplainResult 派生,移除进程级全局计数器与 reset, 修复并发诊断互相踩踏编号导致的重复 node ID 与错挂父子边 - 补充 5 个回归测试文件,含“导出→切分”闭环断言与未转义时的反向对照
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
package app
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
)
|
|
|
|
// parseDistributedMySQLTextExplain parses the single-column text plans emitted
|
|
// by Apache Doris and StarRocks. These plans are PLAN FRAGMENT pipelines, not
|
|
// MySQL's id/type/table tabular EXPLAIN format.
|
|
func parseDistributedMySQLTextExplain(dbType, sourceSQL, raw string, format connection.ExplainFormat) connection.ExplainResult {
|
|
result := connection.ExplainResult{
|
|
DBType: dbType,
|
|
SourceSQL: sourceSQL,
|
|
RawFormat: connection.ExplainFormatText,
|
|
RawPayload: raw,
|
|
}
|
|
|
|
parentID := ""
|
|
lastNodeIndex := -1
|
|
for _, rawLine := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") {
|
|
line := strings.TrimSpace(rawLine)
|
|
if line == "" || strings.HasPrefix(strings.ToUpper(line), "EXPLAIN STRING") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(strings.ToUpper(line), "PLAN FRAGMENT") {
|
|
parentID = ""
|
|
lastNodeIndex = -1
|
|
continue
|
|
}
|
|
|
|
if detail, ok := distributedPlanOperatorDetail(line); ok {
|
|
node := connection.ExplainNode{
|
|
OpType: classifyDistributedPlanOperator(detail),
|
|
OpDetail: detail,
|
|
}
|
|
nodeID := appendExplainChild(&result, parentID, node)
|
|
parentID = nodeID
|
|
lastNodeIndex = len(result.Nodes) - 1
|
|
continue
|
|
}
|
|
|
|
if lastNodeIndex >= 0 {
|
|
applyDistributedPlanProperty(&result.Nodes[lastNodeIndex], line)
|
|
}
|
|
}
|
|
|
|
if len(result.Nodes) == 0 {
|
|
result.Warnings = []string{"未识别到 Doris/StarRocks 计划算子,请查看原文"}
|
|
return result
|
|
}
|
|
finalizeExplainStats(&result)
|
|
return result
|
|
}
|
|
|
|
func distributedPlanOperatorDetail(line string) (string, bool) {
|
|
trimmed := strings.TrimLeft(line, " |+-├└─│\t")
|
|
colon := strings.IndexByte(trimmed, ':')
|
|
if colon <= 0 {
|
|
return "", false
|
|
}
|
|
if _, err := strconv.Atoi(strings.TrimSpace(trimmed[:colon])); err != nil {
|
|
return "", false
|
|
}
|
|
detail := strings.TrimSpace(trimmed[colon+1:])
|
|
upper := strings.ToUpper(detail)
|
|
known := []string{
|
|
"SCAN", "AGGREGATE", "JOIN", "EXCHANGE", "SORT", "TOP-N", "TOPN",
|
|
"LIMIT", "UNION", "PROJECT", "FILTER", "ANALYTIC", "WINDOW", "NODE",
|
|
}
|
|
for _, token := range known {
|
|
if strings.Contains(upper, token) {
|
|
return detail, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func classifyDistributedPlanOperator(detail string) string {
|
|
upper := strings.ToUpper(detail)
|
|
switch {
|
|
case strings.Contains(upper, "SCAN"):
|
|
return connection.ExplainOpScan
|
|
case strings.Contains(upper, "AGGREGATE"):
|
|
return connection.ExplainOpAggregate
|
|
case strings.Contains(upper, "JOIN"):
|
|
return connection.ExplainOpJoin
|
|
case strings.Contains(upper, "SORT"), strings.Contains(upper, "TOP-N"), strings.Contains(upper, "TOPN"):
|
|
return connection.ExplainOpSort
|
|
case strings.Contains(upper, "LIMIT"):
|
|
return connection.ExplainOpLimit
|
|
case strings.Contains(upper, "UNION"):
|
|
return connection.ExplainOpUnion
|
|
case strings.Contains(upper, "FILTER"):
|
|
return connection.ExplainOpFilter
|
|
case strings.Contains(upper, "ANALYTIC"), strings.Contains(upper, "WINDOW"):
|
|
return connection.ExplainOpWindow
|
|
default:
|
|
return connection.ExplainOpOther
|
|
}
|
|
}
|
|
|
|
func applyDistributedPlanProperty(node *connection.ExplainNode, line string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
trimmed := strings.TrimSpace(strings.TrimLeft(line, "|+-├└─│ "))
|
|
lower := strings.ToLower(trimmed)
|
|
switch {
|
|
case strings.HasPrefix(lower, "table:"):
|
|
value := strings.TrimSpace(trimmed[len("table:"):])
|
|
if comma := strings.IndexByte(value, ','); comma >= 0 {
|
|
value = strings.TrimSpace(value[:comma])
|
|
}
|
|
node.Table = value
|
|
case strings.HasPrefix(lower, "cardinality="):
|
|
node.EstRows = parseDistributedPlanInt(strings.TrimSpace(trimmed[len("cardinality="):]))
|
|
case strings.HasPrefix(lower, "cardinality:"):
|
|
node.EstRows = parseDistributedPlanInt(strings.TrimSpace(trimmed[len("cardinality:"):]))
|
|
case strings.HasPrefix(lower, "actualrows="):
|
|
node.ActualRows = parseDistributedPlanInt(strings.TrimSpace(trimmed[len("actualrows="):]))
|
|
case strings.HasPrefix(lower, "rollup:"):
|
|
node.Index = strings.TrimSpace(trimmed[len("rollup:"):])
|
|
case strings.HasPrefix(lower, "partitions=") || strings.HasPrefix(lower, "partitionsratio="):
|
|
ratio := trimmed[strings.IndexByte(trimmed, '=')+1:]
|
|
selected, total, ok := parseDistributedPlanRatio(ratio)
|
|
if ok && selected == total && node.OpType == connection.ExplainOpScan {
|
|
node.Flags = appendUniqueExplainFlags(node.Flags, connection.ExplainFlagFullScan)
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseDistributedPlanInt(value string) int64 {
|
|
end := 0
|
|
for end < len(value) && value[end] >= '0' && value[end] <= '9' {
|
|
end++
|
|
}
|
|
if end == 0 {
|
|
return 0
|
|
}
|
|
parsed, _ := strconv.ParseInt(value[:end], 10, 64)
|
|
return parsed
|
|
}
|
|
|
|
func parseDistributedPlanRatio(value string) (int64, int64, bool) {
|
|
parts := strings.SplitN(strings.TrimSpace(value), "/", 2)
|
|
if len(parts) != 2 {
|
|
return 0, 0, false
|
|
}
|
|
selected := parseDistributedPlanInt(parts[0])
|
|
total := parseDistributedPlanInt(parts[1])
|
|
return selected, total, total > 0
|
|
}
|
|
|
|
func appendUniqueExplainFlags(flags []string, values ...string) []string {
|
|
for _, value := range values {
|
|
found := false
|
|
for _, existing := range flags {
|
|
if existing == value {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
flags = append(flags, value)
|
|
}
|
|
}
|
|
return flags
|
|
}
|