publish v1.5.1
This commit is contained in:
parent
aad6d3a217
commit
74b9bfe511
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# 变更记录 - @go/db
|
||||
|
||||
## [1.5.1] - 2026-06-04
|
||||
- **架构重构: 两阶段 (Two-Pass) 索引命名生成**:
|
||||
- 重构了 `CheckTable` 中的 Schema 同步逻辑,从“边解析边生成”改为两阶段处理。现在多列联合索引(无论是 Unique, Index 还是 Fulltext)的名字会根据包含的**字段名称自动拼接生成**(如 `uk_user_name_phone`),替代了以前难以溯源的分组号(如 `uk_user_1`),极大提升了 DBA 在数据库维护时的可读性与直观性。
|
||||
- **超长防断机制**: 当生成的索引名称超过 60 个字符时,自动进行截断,并在末尾追加原完整名称的 6 位 MD5 Hash,完美规避了各种 RDBMS 对标识符 64 字符长度限制引发的建表错误。
|
||||
- **稳定性修复: 幂等性增强**:
|
||||
- 为所有支持的数据库(特别是 SQLite 和 PostgreSQL)的 `CREATE INDEX` 及 `CREATE UNIQUE INDEX` 语句增加了 `IF NOT EXISTS` 容错保护。
|
||||
- **SQLite 特效探测**: 修复了对 SQLite 多列索引结构探测的 Bug,现在它能准确匹配基于列组隐式创建的索引(如 `sqlite_autoindex`),并将其视作等效的已存在约束跳过重建。彻底根除了在系统重启及结构同步期间偶现的 `UNIQUE constraint failed` 致命崩溃。
|
||||
|
||||
## [1.5.0] - 2026-05-10
|
||||
- **基础设施对齐**: 全局对齐至 v1.5.0。
|
||||
|
||||
## [1.3.5] - 2026-05-30
|
||||
- **新增**: 注册到 `jsmod`。
|
||||
- **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode` 下,仅允许读取操作(Query),所有写操作(Exec/Insert/Update/Delete/Replace)将被拦截并返回错误。严禁通过 JS 动态创建连接或同步 Schema。
|
||||
|
||||
153
Schema.go
153
Schema.go
@ -1,6 +1,8 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -321,84 +323,106 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
||||
isPostgres := db.Config.Type == "pg" || db.Config.Type == "pgsql" || db.Config.Type == "postgres"
|
||||
|
||||
table.Columns = make([]string, 0, len(table.Fields))
|
||||
|
||||
// 阶段一:收集所有属于同一索引组的字段
|
||||
type indexGroupDef struct {
|
||||
Type string
|
||||
Fields []string
|
||||
}
|
||||
indexGroups := make(map[string]*indexGroupDef)
|
||||
|
||||
for i, field := range table.Fields {
|
||||
field.Parse(db.Config.Type)
|
||||
table.Fields[i] = field
|
||||
table.Columns = append(table.Columns, field.Name)
|
||||
|
||||
switch strings.ToLower(field.Index) {
|
||||
case "pk", "primary key":
|
||||
idxType := strings.ToLower(field.Index)
|
||||
if idxType == "pk" || idxType == "primary key" {
|
||||
pks = append(pks, field.Name)
|
||||
case "unique":
|
||||
keyName := fmt.Sprint("uk_", table.Name, "_", field.Name)
|
||||
if field.IndexGroup != "" {
|
||||
keyName = fmt.Sprint("uk_", table.Name, "_", field.IndexGroup)
|
||||
} else if idxType == "unique" || idxType == "index" || idxType == "fulltext" {
|
||||
group := field.IndexGroup
|
||||
if group == "" {
|
||||
group = field.Name
|
||||
}
|
||||
if keySetBy[keyName] != "" {
|
||||
keySetFields[keyName] += " " + field.Name
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", "+db.Quote(field.Name)+")", 1)
|
||||
} else if isPostgres {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", "+db.Quote(field.Name)+")", 1)
|
||||
} else {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ") COMMENT", ", "+db.Quote(field.Name)+") COMMENT", 1)
|
||||
groupKey := idxType + "_" + group
|
||||
if _, exists := indexGroups[groupKey]; !exists {
|
||||
indexGroups[groupKey] = &indexGroupDef{Type: idxType, Fields: []string{}}
|
||||
}
|
||||
} else {
|
||||
keySetFields[keyName] = field.Name
|
||||
keySet := ""
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
|
||||
keySet = fmt.Sprintf("CREATE UNIQUE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name)
|
||||
} else if isPostgres {
|
||||
keySet = fmt.Sprintf("CREATE UNIQUE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name)
|
||||
} else {
|
||||
keySet = fmt.Sprintf("UNIQUE KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment)
|
||||
indexGroups[groupKey].Fields = append(indexGroups[groupKey].Fields, field.Name)
|
||||
}
|
||||
keySets = append(keySets, keySet)
|
||||
keySetBy[keyName] = keySet
|
||||
}
|
||||
case "fulltext":
|
||||
ftsFields = append(ftsFields, field.Name)
|
||||
keyName := fmt.Sprint("tk_", table.Name, "_", field.Name)
|
||||
|
||||
// 阶段二:生成字段定义与索引定义
|
||||
processedGroups := make(map[string]bool)
|
||||
|
||||
for _, field := range table.Fields {
|
||||
idxType := strings.ToLower(field.Index)
|
||||
if idxType == "unique" || idxType == "index" || idxType == "fulltext" {
|
||||
group := field.IndexGroup
|
||||
if group == "" {
|
||||
group = field.Name
|
||||
}
|
||||
groupKey := idxType + "_" + group
|
||||
|
||||
if !processedGroups[groupKey] {
|
||||
processedGroups[groupKey] = true
|
||||
groupDef := indexGroups[groupKey]
|
||||
|
||||
// 根据组合的字段名拼接生成长索引名
|
||||
joinedFields := strings.Join(groupDef.Fields, "_")
|
||||
prefix := "ik"
|
||||
if idxType == "unique" {
|
||||
prefix = "uk"
|
||||
} else if idxType == "fulltext" {
|
||||
prefix = "tk"
|
||||
}
|
||||
keyName := fmt.Sprintf("%s_%s_%s", prefix, table.Name, joinedFields)
|
||||
|
||||
// 防超长截断处理 (最大 64,留 4 个给安全余量,设为 60)
|
||||
if len(keyName) > 60 {
|
||||
hash := md5.Sum([]byte(keyName))
|
||||
hashStr := hex.EncodeToString(hash[:])[:6]
|
||||
keyName = keyName[:53] + "_" + hashStr
|
||||
}
|
||||
|
||||
keySetFields[keyName] = strings.Join(groupDef.Fields, " ")
|
||||
|
||||
if idxType == "fulltext" {
|
||||
ftsFields = append(ftsFields, groupDef.Fields...)
|
||||
keySet := ""
|
||||
if isPostgres {
|
||||
// 使用 simple 分词器,配合应用层的分词结果
|
||||
keySet = fmt.Sprintf("CREATE INDEX \"%s\" ON \"%s\" USING GIN (to_tsvector('simple', \"%s\"))", keyName, table.Name, field.Name)
|
||||
// PostgreSQL 使用 simple 分词器
|
||||
keySet = fmt.Sprintf("CREATE INDEX IF NOT EXISTS \"%s\" ON \"%s\" USING GIN (to_tsvector('simple', \"%s\"))", keyName, table.Name, groupDef.Fields[0])
|
||||
} else if !strings.HasPrefix(db.Config.Type, "sqlite") && db.Config.Type != "chai" {
|
||||
keySet = fmt.Sprintf("FULLTEXT KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment)
|
||||
} else {
|
||||
// SQLite 使用 FTS5,这里不生成普通索引
|
||||
keySet = ""
|
||||
keySet = fmt.Sprintf("FULLTEXT KEY "+db.Quote("%s")+" ("+db.Quotes(groupDef.Fields)+") COMMENT '%s'", keyName, field.Comment)
|
||||
}
|
||||
if keySet != "" {
|
||||
keySets = append(keySets, keySet)
|
||||
keySetBy[keyName] = keySet
|
||||
}
|
||||
case "index":
|
||||
keyName := fmt.Sprint("ik_", table.Name, "_", field.Name)
|
||||
if field.IndexGroup != "" {
|
||||
keyName = fmt.Sprint("ik_", table.Name, "_", field.IndexGroup)
|
||||
}
|
||||
if keySetBy[keyName] != "" {
|
||||
keySetFields[keyName] += " " + field.Name
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", \""+field.Name+"\")", 1)
|
||||
} else if isPostgres {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", \""+field.Name+"\")", 1)
|
||||
} else {
|
||||
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ") COMMENT", ", `"+field.Name+"`) COMMENT", 1)
|
||||
}
|
||||
} else {
|
||||
keySetFields[keyName] = field.Name
|
||||
} else if idxType == "unique" {
|
||||
keySet := ""
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
|
||||
keySet = fmt.Sprintf("CREATE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name)
|
||||
keySet = fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
|
||||
} else if isPostgres {
|
||||
keySet = fmt.Sprintf("CREATE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name)
|
||||
keySet = fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
|
||||
} else {
|
||||
keySet = fmt.Sprintf("KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment)
|
||||
keySet = fmt.Sprintf("UNIQUE KEY "+db.Quote("%s")+" ("+db.Quotes(groupDef.Fields)+") COMMENT '%s'", keyName, field.Comment)
|
||||
}
|
||||
keySets = append(keySets, keySet)
|
||||
keySetBy[keyName] = keySet
|
||||
} else { // index
|
||||
keySet := ""
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
|
||||
keySet = fmt.Sprintf("CREATE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
|
||||
} else if isPostgres {
|
||||
keySet = fmt.Sprintf("CREATE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
|
||||
} else {
|
||||
keySet = fmt.Sprintf("KEY "+db.Quote("%s")+" ("+db.Quotes(groupDef.Fields)+") COMMENT '%s'", keyName, field.Comment)
|
||||
}
|
||||
keySets = append(keySets, keySet)
|
||||
keySetBy[keyName] = keySet
|
||||
}
|
||||
}
|
||||
}
|
||||
fieldSets = append(fieldSets, field.Desc)
|
||||
@ -419,6 +443,7 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
||||
oldFieldList := make([]*tableFieldDesc, 0)
|
||||
oldFields := make(map[string]*tableFieldDesc)
|
||||
oldIndexes := make(map[string]string)
|
||||
oldUniqueMap := make(map[string]bool)
|
||||
oldIndexInfos := make([]*tableKeyDesc, 0)
|
||||
oldComments := map[string]string{}
|
||||
|
||||
@ -452,6 +477,7 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
||||
return err
|
||||
}
|
||||
for _, i := range tmpIndexes {
|
||||
oldUniqueMap[i.Name] = i.Unique == 1
|
||||
tmpIndexInfo := []struct {
|
||||
Name string
|
||||
Seqno int
|
||||
@ -460,10 +486,10 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
||||
if err := db.Query("PRAGMA index_info(" + db.Quote(i.Name) + ")").To(&tmpIndexInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tmpIndexInfo) > 0 {
|
||||
for _, ii := range tmpIndexInfo {
|
||||
oldIndexInfos = append(oldIndexInfos, &tableKeyDesc{
|
||||
Key_name: i.Name,
|
||||
Column_name: tmpIndexInfo[0].Name,
|
||||
Column_name: ii.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -602,7 +628,22 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
||||
|
||||
for keyId, keySet := range keySetBy {
|
||||
if oldIndexes[keyId] == "" || (!isPostgres && strings.ToLower(oldIndexes[keyId]) != strings.ToLower(keySetFields[keyId])) {
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") || isPostgres {
|
||||
if strings.HasPrefix(db.Config.Type, "sqlite") {
|
||||
// 针对 SQLite 优化:如果索引名不匹配,但存在一个相同列组的唯一索引(如 sqlite_autoindex),则跳过
|
||||
if strings.Contains(keySet, "UNIQUE") {
|
||||
foundUnique := false
|
||||
for oldName, oldCols := range oldIndexes {
|
||||
if oldUniqueMap[oldName] && strings.ToLower(oldCols) == strings.ToLower(keySetFields[keyId]) {
|
||||
foundUnique = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundUnique {
|
||||
continue
|
||||
}
|
||||
}
|
||||
actions = append(actions, keySet)
|
||||
} else if isPostgres {
|
||||
actions = append(actions, keySet)
|
||||
} else {
|
||||
actions = append(actions, "ADD "+keySet)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user