mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-20 13:39:43 +08:00
🐛 fix(sql-editor): 修复脚本执行拆分与元数据只读提示
- Oracle 匿名块:识别 BEGIN/DECLARE...END 块,避免按内部分号错误拆分 - 执行路径:PL/SQL 块跳过批量写入路径,保持单条语句语义 - SQL 文件:同步修复流式 SQL 文件拆分逻辑 - 查询结果:系统元数据表保持只读但不再弹业务表主键提示 - 测试覆盖:补充前后端拆分、执行和 information_schema 回归用例
This commit is contained in:
@@ -3,8 +3,9 @@ package app
|
||||
import "strings"
|
||||
|
||||
// splitSQLStatements 按分号拆分 SQL 文本为独立语句。
|
||||
// 正确处理单引号/双引号/反引号字符串、行注释(-- / #)、块注释(/* */)和
|
||||
// PostgreSQL/Kingbase 的 $$...$$ dollar-quoting,避免在这些上下文中错误拆分。
|
||||
// 正确处理单引号/双引号/反引号字符串、行注释(-- / #)、块注释(/* */)、
|
||||
// PostgreSQL/Kingbase 的 $$...$$ dollar-quoting,以及 Oracle PL/SQL 匿名块,
|
||||
// 避免在这些上下文中错误拆分。
|
||||
// 同时支持 SQL 标准的转义单引号(两个连续单引号 '' 表示字面量引号)。
|
||||
func splitSQLStatements(sql string) []string {
|
||||
text := strings.ReplaceAll(sql, "\r\n", "\n")
|
||||
@@ -18,6 +19,9 @@ func splitSQLStatements(sql string) []string {
|
||||
inLineComment := false
|
||||
inBlockComment := false
|
||||
var dollarTag string // postgres/kingbase: $$...$$ or $tag$...$tag$
|
||||
plsqlDepth := 0
|
||||
plsqlDeclareBeginSkips := 0
|
||||
justClosedPLSQLBlock := false
|
||||
|
||||
push := func() {
|
||||
s := strings.TrimSpace(cur.String())
|
||||
@@ -108,6 +112,35 @@ func splitSQLStatements(sql string) []string {
|
||||
continue
|
||||
}
|
||||
|
||||
if isSQLIdentifierStart(ch) {
|
||||
tokenStart := i
|
||||
tokenEnd := i + 1
|
||||
for tokenEnd < len(text) && isSQLIdentifierPart(text[tokenEnd]) {
|
||||
tokenEnd++
|
||||
}
|
||||
token := strings.ToLower(text[tokenStart:tokenEnd])
|
||||
if token == "begin" && plsqlDeclareBeginSkips > 0 {
|
||||
plsqlDeclareBeginSkips--
|
||||
justClosedPLSQLBlock = false
|
||||
} else if token == "begin" && shouldEnterPLSQLBlock(text, tokenEnd) {
|
||||
plsqlDepth++
|
||||
justClosedPLSQLBlock = false
|
||||
} else if token == "declare" && shouldEnterPLSQLDeclareBlock(text, tokenEnd) {
|
||||
plsqlDepth++
|
||||
plsqlDeclareBeginSkips++
|
||||
justClosedPLSQLBlock = false
|
||||
} else if token == "end" && plsqlDepth > 0 && !isPLSQLControlEnd(text, tokenEnd) {
|
||||
plsqlDepth--
|
||||
if plsqlDeclareBeginSkips > plsqlDepth {
|
||||
plsqlDeclareBeginSkips = plsqlDepth
|
||||
}
|
||||
justClosedPLSQLBlock = plsqlDepth == 0
|
||||
}
|
||||
cur.WriteString(text[tokenStart:tokenEnd])
|
||||
i = tokenEnd - 1
|
||||
continue
|
||||
}
|
||||
|
||||
// 行注释开始
|
||||
if ch == '-' && next == '-' {
|
||||
inLineComment = true
|
||||
@@ -140,11 +173,33 @@ func splitSQLStatements(sql string) []string {
|
||||
|
||||
// 分号分隔(支持全角分号";")
|
||||
if ch == ';' {
|
||||
if plsqlDepth > 0 {
|
||||
cur.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
if justClosedPLSQLBlock {
|
||||
cur.WriteByte(ch)
|
||||
push()
|
||||
justClosedPLSQLBlock = false
|
||||
continue
|
||||
}
|
||||
push()
|
||||
continue
|
||||
}
|
||||
// 全角分号 UTF-8 序列: 0xEF 0xBC 0x9B
|
||||
if ch == 0xEF && i+2 < len(text) && text[i+1] == 0xBC && text[i+2] == 0x9B {
|
||||
if plsqlDepth > 0 {
|
||||
cur.WriteString(";")
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if justClosedPLSQLBlock {
|
||||
cur.WriteString(";")
|
||||
push()
|
||||
justClosedPLSQLBlock = false
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
push()
|
||||
i += 2
|
||||
continue
|
||||
@@ -157,6 +212,110 @@ func splitSQLStatements(sql string) []string {
|
||||
return statements
|
||||
}
|
||||
|
||||
func isSQLIdentifierStart(ch byte) bool {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
||||
}
|
||||
|
||||
func isSQLIdentifierPart(ch byte) bool {
|
||||
return isSQLIdentifierStart(ch) || (ch >= '0' && ch <= '9') || ch == '$' || ch == '#'
|
||||
}
|
||||
|
||||
func skipSQLWhitespaceAndComments(text string, pos int) int {
|
||||
i := pos
|
||||
for i < len(text) {
|
||||
switch text[i] {
|
||||
case ' ', '\t', '\n', '\r', '\f':
|
||||
i++
|
||||
continue
|
||||
case '-':
|
||||
if i+1 < len(text) && text[i+1] == '-' {
|
||||
i += 2
|
||||
for i < len(text) && text[i] != '\n' {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
case '/':
|
||||
if i+1 < len(text) && text[i+1] == '*' {
|
||||
i += 2
|
||||
for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
|
||||
i++
|
||||
}
|
||||
if i+1 < len(text) {
|
||||
i += 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func nextSQLSignificantToken(text string, pos int) string {
|
||||
i := skipSQLWhitespaceAndComments(text, pos)
|
||||
if i >= len(text) || !isSQLIdentifierStart(text[i]) {
|
||||
return ""
|
||||
}
|
||||
end := i + 1
|
||||
for end < len(text) && isSQLIdentifierPart(text[end]) {
|
||||
end++
|
||||
}
|
||||
return strings.ToLower(text[i:end])
|
||||
}
|
||||
|
||||
func nextSQLSignificantByte(text string, pos int) byte {
|
||||
i := skipSQLWhitespaceAndComments(text, pos)
|
||||
if i >= len(text) {
|
||||
return 0
|
||||
}
|
||||
return text[i]
|
||||
}
|
||||
|
||||
func shouldEnterPLSQLBlock(text string, tokenEnd int) bool {
|
||||
switch nextSQLSignificantByte(text, tokenEnd) {
|
||||
case 0, ';':
|
||||
return false
|
||||
}
|
||||
switch nextSQLSignificantToken(text, tokenEnd) {
|
||||
case "transaction", "work", "isolation", "read", "write":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func isPLSQLBlockStatement(stmt string) bool {
|
||||
text := strings.TrimSpace(stmt)
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasSuffix(text, "/") {
|
||||
text = strings.TrimSpace(strings.TrimSuffix(text, "/"))
|
||||
}
|
||||
token := nextSQLSignificantToken(text, 0)
|
||||
if token == "declare" {
|
||||
return shouldEnterPLSQLDeclareBlock(text, len("declare"))
|
||||
}
|
||||
if token != "begin" {
|
||||
return false
|
||||
}
|
||||
return shouldEnterPLSQLBlock(text, len("begin"))
|
||||
}
|
||||
|
||||
func shouldEnterPLSQLDeclareBlock(text string, tokenEnd int) bool {
|
||||
return nextSQLSignificantToken(text, tokenEnd) != ""
|
||||
}
|
||||
|
||||
func isPLSQLControlEnd(text string, tokenEnd int) bool {
|
||||
switch nextSQLSignificantToken(text, tokenEnd) {
|
||||
case "if", "loop", "case":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// parseSQLDollarTag 解析 PostgreSQL/Kingbase 的 dollar-quoting 标签。
|
||||
func parseSQLDollarTag(s string) string {
|
||||
if len(s) < 2 || s[0] != '$' {
|
||||
|
||||
Reference in New Issue
Block a user