publish v1.5.1

This commit is contained in:
AI Engineer 2026-06-04 20:48:57 +08:00
parent aad6d3a217
commit 74b9bfe511
2 changed files with 123 additions and 71 deletions

View File

@ -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
View File

@ -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)