保留原始视图定义与编码,按依赖排序导出,并严格识别和重排官方最终视图块;增加运行上下文和权限检查。补充视图、清单、恢复失败保护测试,并修正旧事件、触发器和例程测试的权限前提。 隔离MySQL 5.7和8.0完整专项分别通过176和202个叶子用例,无失败或跳过;后端常规全量、静态检查、模块校验及Linux测试包交叉编译通过。测试产物和凭据未提交。本提交不代表生产部署验收完成。
199 lines
5.6 KiB
Go
199 lines
5.6 KiB
Go
package cli
|
||
|
||
import (
|
||
"fmt"
|
||
"maps"
|
||
"strings"
|
||
)
|
||
|
||
func orderMySQLViews(views []mysqlView, schema string, lowerCaseNames int) ([]mysqlView, error) {
|
||
normalize := func(value string) string {
|
||
if lowerCaseNames != 0 {
|
||
return strings.ToLower(value)
|
||
}
|
||
return value
|
||
}
|
||
index := make(map[string]int, len(views))
|
||
for i, view := range views {
|
||
key := normalize(view.name)
|
||
if _, exists := index[key]; exists {
|
||
return nil, fmt.Errorf("MySQL 视图名称重复:%s", view.name)
|
||
}
|
||
index[key] = i
|
||
}
|
||
dependencies := make([][]int, len(views))
|
||
for i, view := range views {
|
||
tokens, err := lexMySQLView(view.definition["Create View"])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析 MySQL 视图失败(%s):%w", view.name, err)
|
||
}
|
||
// 从 VIEW 之后的 AS 开始分析,不把定义者或可选列清单当作查询。
|
||
seenView, start := false, -1
|
||
for j, token := range tokens {
|
||
seenView = seenView || token.keyword("VIEW")
|
||
if seenView && token.keyword("AS") {
|
||
start = j + 1
|
||
break
|
||
}
|
||
}
|
||
if start < 0 || start >= len(tokens) {
|
||
return nil, fmt.Errorf("MySQL 视图查询定义不完整:%s", view.name)
|
||
}
|
||
seen := make(map[int]bool)
|
||
add := func(database, name string) {
|
||
if database != "" && normalize(database) != normalize(schema) {
|
||
return
|
||
}
|
||
if target, ok := index[normalize(name)]; ok && !seen[target] {
|
||
seen[target] = true
|
||
dependencies[i] = append(dependencies[i], target)
|
||
}
|
||
}
|
||
if err := walkMySQLViewQuery(tokens[start:], nil, false, 0, add); err != nil {
|
||
return nil, fmt.Errorf("解析 MySQL 视图依赖失败(%s):%w", view.name, err)
|
||
}
|
||
}
|
||
state := make([]byte, len(views))
|
||
ordered := make([]mysqlView, 0, len(views))
|
||
var visit func(int) error
|
||
visit = func(i int) error {
|
||
if state[i] == 2 {
|
||
return nil
|
||
}
|
||
if state[i] == 1 {
|
||
return fmt.Errorf("MySQL 视图依赖存在循环:%s", views[i].name)
|
||
}
|
||
state[i] = 1
|
||
for _, dependency := range dependencies[i] {
|
||
if err := visit(dependency); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
state[i] = 2
|
||
ordered = append(ordered, views[i])
|
||
return nil
|
||
}
|
||
for i := range views {
|
||
if err := visit(i); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return ordered, nil
|
||
}
|
||
|
||
// 只在查询块的 FROM/JOIN 表位置记录依赖;递归分析子查询和括号连接。
|
||
// TRIM/EXTRACT 中的 FROM、列名、别名和函数名都不是表位置。
|
||
func walkMySQLViewQuery(tokens []mysqlViewToken, inherited map[string]bool, tableGroup bool, depth int, add func(string, string)) error {
|
||
if depth > 256 {
|
||
return fmt.Errorf("MySQL 视图查询嵌套过深")
|
||
}
|
||
ctes := maps.Clone(inherited)
|
||
if ctes == nil {
|
||
ctes = make(map[string]bool)
|
||
}
|
||
i := 0
|
||
if len(tokens) > 0 && tokens[0].keyword("WITH") {
|
||
i++
|
||
recursive := i < len(tokens) && tokens[i].keyword("RECURSIVE")
|
||
if recursive {
|
||
i++
|
||
}
|
||
for {
|
||
if i >= len(tokens) || !tokens[i].identifier() {
|
||
return fmt.Errorf("MySQL 视图公共表表达式名称缺失")
|
||
}
|
||
name := strings.ToLower(tokens[i].text)
|
||
i++
|
||
if i < len(tokens) && tokens[i].text == "(" && tokens[i].kind == 'p' {
|
||
end, err := mysqlViewGroupEnd(tokens, i)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
i = end + 1
|
||
}
|
||
if i+1 >= len(tokens) || !tokens[i].keyword("AS") || tokens[i+1].text != "(" || tokens[i+1].kind != 'p' {
|
||
return fmt.Errorf("MySQL 视图公共表表达式定义不完整")
|
||
}
|
||
i++
|
||
end, err := mysqlViewGroupEnd(tokens, i)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if recursive {
|
||
ctes[name] = true
|
||
}
|
||
if err := walkMySQLViewQuery(tokens[i+1:end], ctes, false, depth+1, add); err != nil {
|
||
return err
|
||
}
|
||
ctes[name] = true
|
||
i = end + 1
|
||
if i >= len(tokens) || tokens[i].kind != 'p' || tokens[i].text != "," {
|
||
break
|
||
}
|
||
i++
|
||
}
|
||
}
|
||
selectSeen, inFrom, expectTable := false, tableGroup, tableGroup
|
||
for ; i < len(tokens); i++ {
|
||
token := tokens[i]
|
||
if token.kind == 'p' && token.text == "(" {
|
||
end, err := mysqlViewGroupEnd(tokens, i)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := walkMySQLViewQuery(tokens[i+1:end], ctes, expectTable, depth+1, add); err != nil {
|
||
return err
|
||
}
|
||
i, expectTable = end, false
|
||
continue
|
||
}
|
||
if token.kind == 'p' && token.text == ")" {
|
||
return fmt.Errorf("MySQL 视图查询括号不匹配")
|
||
}
|
||
switch {
|
||
case token.keyword("SELECT"):
|
||
selectSeen, inFrom, expectTable = true, false, false
|
||
case token.keyword("FROM") && selectSeen:
|
||
inFrom, expectTable = true, true
|
||
case (token.keyword("JOIN") || token.keyword("STRAIGHT_JOIN")) && inFrom:
|
||
expectTable = true
|
||
case token.kind == 'p' && token.text == "," && inFrom:
|
||
expectTable = true
|
||
case token.kind == 'w' && mysqlViewFromBoundary(token.text):
|
||
inFrom, expectTable = false, false
|
||
case expectTable:
|
||
if token.keyword("LATERAL") {
|
||
continue
|
||
}
|
||
if !token.identifier() {
|
||
return fmt.Errorf("MySQL 视图表引用格式无法识别")
|
||
}
|
||
database, name := "", token.text
|
||
if i+1 < len(tokens) && tokens[i+1].kind == 'p' && tokens[i+1].text == "." {
|
||
if i+2 >= len(tokens) || !tokens[i+2].identifier() {
|
||
return fmt.Errorf("MySQL 视图限定表名不完整")
|
||
}
|
||
database, name = name, tokens[i+2].text
|
||
i += 2
|
||
}
|
||
function := i+1 < len(tokens) && tokens[i+1].kind == 'p' && tokens[i+1].text == "("
|
||
if !function && (database != "" || !ctes[strings.ToLower(name)]) {
|
||
add(database, name)
|
||
}
|
||
expectTable = false
|
||
}
|
||
}
|
||
if expectTable {
|
||
return fmt.Errorf("MySQL 视图表引用缺失")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func mysqlViewFromBoundary(word string) bool {
|
||
switch strings.ToUpper(word) {
|
||
case "WHERE", "GROUP", "HAVING", "ORDER", "LIMIT", "UNION", "EXCEPT", "INTERSECT", "WINDOW", "INTO", "FOR", "LOCK":
|
||
return true
|
||
}
|
||
return false
|
||
}
|