feat(query): 支持多条SQL语句执行返回多结果集

- 新增 ResultSetData 结构体承载单个结果集数据
- 新增 MultiResultQuerier/MultiResultQuerierContext 可选接口
- 新增 scanMultiRows 函数利用 NextResultSet() 遍历所有结果集
- MySQL 驱动 DSN 开启 multiStatements=true 并实现多结果集接口
- 新增 DBQueryMulti Wails 方法,支持驱动原生多结果集及自动回退逐条执行
- 新增 Go 版 SQL 拆分函数 splitSQLStatements 及 10 个单元测试
- 前端 QueryEditor handleRun 改为一次性调用 DBQueryMulti
- MongoDB 保持独立的逐条执行路径不受影响
- refs #235
This commit is contained in:
杨国锋
2026-03-17 22:21:49 +08:00
parent 064cdc34be
commit 0ab10d2e80
10 changed files with 682 additions and 106 deletions

167
internal/app/sql_split.go Normal file
View File

@@ -0,0 +1,167 @@
package app
import "strings"
// splitSQLStatements 按分号拆分 SQL 文本为独立语句。
// 正确处理单引号/双引号/反引号字符串、行注释(-- / #)、块注释(/* */)和
// PostgreSQL/Kingbase 的 $$...$$ dollar-quoting避免在这些上下文中错误拆分。
func splitSQLStatements(sql string) []string {
text := strings.ReplaceAll(sql, "\r\n", "\n")
var statements []string
cur := ""
inSingle := false
inDouble := false
inBacktick := false
escaped := false
inLineComment := false
inBlockComment := false
var dollarTag string // postgres/kingbase: $$...$$ or $tag$...$tag$
push := func() {
s := strings.TrimSpace(cur)
if s != "" {
statements = append(statements, s)
}
cur = ""
}
for i := 0; i < len(text); i++ {
ch := text[i]
next := byte(0)
if i+1 < len(text) {
next = text[i+1]
}
// 行注释
if inLineComment {
if ch == '\n' {
inLineComment = false
}
cur += string(ch)
continue
}
// 块注释
if inBlockComment {
cur += string(ch)
if ch == '*' && next == '/' {
cur += "/"
i++
inBlockComment = false
}
continue
}
// Dollar-quoting
if dollarTag != "" {
if strings.HasPrefix(text[i:], dollarTag) {
cur += dollarTag
i += len(dollarTag) - 1
dollarTag = ""
} else {
cur += string(ch)
}
continue
}
// 转义字符
if escaped {
escaped = false
cur += string(ch)
continue
}
if (inSingle || inDouble) && ch == '\\' {
escaped = true
cur += string(ch)
continue
}
// 字符串开闭
if !inDouble && !inBacktick && ch == '\'' {
inSingle = !inSingle
cur += string(ch)
continue
}
if !inSingle && !inBacktick && ch == '"' {
inDouble = !inDouble
cur += string(ch)
continue
}
if !inSingle && !inDouble && ch == '`' {
inBacktick = !inBacktick
cur += string(ch)
continue
}
// 在引号/反引号内部不做任何判断
if inSingle || inDouble || inBacktick {
cur += string(ch)
continue
}
// 行注释开始
if ch == '-' && next == '-' {
inLineComment = true
cur += string(ch)
continue
}
if ch == '#' {
inLineComment = true
cur += string(ch)
continue
}
// 块注释开始
if ch == '/' && next == '*' {
inBlockComment = true
cur += "/*"
i++
continue
}
// Dollar-quoting 开始
if ch == '$' {
if tag := parseSQLDollarTag(text[i:]); tag != "" {
dollarTag = tag
cur += tag
i += len(tag) - 1
continue
}
}
// 分号分隔(支持全角分号""
if ch == ';' {
push()
continue
}
// 全角分号 UTF-8 序列: 0xEF 0xBC 0x9B
if ch == 0xEF && i+2 < len(text) && text[i+1] == 0xBC && text[i+2] == 0x9B {
push()
i += 2
continue
}
cur += string(ch)
}
push()
return statements
}
// parseSQLDollarTag 解析 PostgreSQL/Kingbase 的 dollar-quoting 标签。
func parseSQLDollarTag(s string) string {
if len(s) < 2 || s[0] != '$' {
return ""
}
for i := 1; i < len(s); i++ {
c := s[i]
if c == '$' {
return s[:i+1]
}
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
return ""
}
}
return ""
}