mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
- Oracle 匿名块:识别 BEGIN/DECLARE...END 块,避免按内部分号错误拆分 - 执行路径:PL/SQL 块跳过批量写入路径,保持单条语句语义 - SQL 文件:同步修复流式 SQL 文件拆分逻辑 - 查询结果:系统元数据表保持只读但不再弹业务表主键提示 - 测试覆盖:补充前后端拆分、执行和 information_schema 回归用例
335 lines
7.2 KiB
Go
335 lines
7.2 KiB
Go
package app
|
||
|
||
import "strings"
|
||
|
||
// splitSQLStatements 按分号拆分 SQL 文本为独立语句。
|
||
// 正确处理单引号/双引号/反引号字符串、行注释(-- / #)、块注释(/* */)、
|
||
// PostgreSQL/Kingbase 的 $$...$$ dollar-quoting,以及 Oracle PL/SQL 匿名块,
|
||
// 避免在这些上下文中错误拆分。
|
||
// 同时支持 SQL 标准的转义单引号(两个连续单引号 '' 表示字面量引号)。
|
||
func splitSQLStatements(sql string) []string {
|
||
text := strings.ReplaceAll(sql, "\r\n", "\n")
|
||
var statements []string
|
||
|
||
var cur strings.Builder
|
||
inSingle := false
|
||
inDouble := false
|
||
inBacktick := false
|
||
escaped := false
|
||
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())
|
||
if s != "" {
|
||
statements = append(statements, s)
|
||
}
|
||
cur.Reset()
|
||
}
|
||
|
||
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.WriteByte(ch)
|
||
continue
|
||
}
|
||
|
||
// 块注释
|
||
if inBlockComment {
|
||
cur.WriteByte(ch)
|
||
if ch == '*' && next == '/' {
|
||
cur.WriteByte('/')
|
||
i++
|
||
inBlockComment = false
|
||
}
|
||
continue
|
||
}
|
||
|
||
// Dollar-quoting
|
||
if dollarTag != "" {
|
||
if strings.HasPrefix(text[i:], dollarTag) {
|
||
cur.WriteString(dollarTag)
|
||
i += len(dollarTag) - 1
|
||
dollarTag = ""
|
||
} else {
|
||
cur.WriteByte(ch)
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 转义字符(反斜杠转义,MySQL 风格)
|
||
if escaped {
|
||
escaped = false
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
if (inSingle || inDouble) && ch == '\\' {
|
||
escaped = true
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
|
||
// 字符串开闭
|
||
if !inDouble && !inBacktick && ch == '\'' {
|
||
if inSingle && next == '\'' {
|
||
// SQL 标准转义:两个连续单引号 '' 表示字面量引号,保持在引号内
|
||
cur.WriteByte(ch)
|
||
cur.WriteByte(next)
|
||
i++
|
||
continue
|
||
}
|
||
inSingle = !inSingle
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
if !inSingle && !inBacktick && ch == '"' {
|
||
inDouble = !inDouble
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
if !inSingle && !inDouble && ch == '`' {
|
||
inBacktick = !inBacktick
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
|
||
// 在引号/反引号内部不做任何判断
|
||
if inSingle || inDouble || inBacktick {
|
||
cur.WriteByte(ch)
|
||
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
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
if ch == '#' {
|
||
inLineComment = true
|
||
cur.WriteByte(ch)
|
||
continue
|
||
}
|
||
|
||
// 块注释开始
|
||
if ch == '/' && next == '*' {
|
||
inBlockComment = true
|
||
cur.WriteString("/*")
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// Dollar-quoting 开始
|
||
if ch == '$' {
|
||
if tag := parseSQLDollarTag(text[i:]); tag != "" {
|
||
dollarTag = tag
|
||
cur.WriteString(tag)
|
||
i += len(tag) - 1
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 分号分隔(支持全角分号";")
|
||
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
|
||
}
|
||
|
||
cur.WriteByte(ch)
|
||
}
|
||
|
||
push()
|
||
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] != '$' {
|
||
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 ""
|
||
}
|