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 # 变更记录 - @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 ## [1.3.5] - 2026-05-30
- **新增**: 注册到 `jsmod` - **新增**: 注册到 `jsmod`
- **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode`仅允许读取操作Query所有写操作Exec/Insert/Update/Delete/Replace将被拦截并返回错误。严禁通过 JS 动态创建连接或同步 Schema。 - **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode`仅允许读取操作Query所有写操作Exec/Insert/Update/Delete/Replace将被拦截并返回错误。严禁通过 JS 动态创建连接或同步 Schema。

183
Schema.go
View File

@ -1,6 +1,8 @@
package db package db
import ( import (
"crypto/md5"
"encoding/hex"
"fmt" "fmt"
"regexp" "regexp"
"strings" "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" isPostgres := db.Config.Type == "pg" || db.Config.Type == "pgsql" || db.Config.Type == "postgres"
table.Columns = make([]string, 0, len(table.Fields)) 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 { for i, field := range table.Fields {
field.Parse(db.Config.Type) field.Parse(db.Config.Type)
table.Fields[i] = field table.Fields[i] = field
table.Columns = append(table.Columns, field.Name) table.Columns = append(table.Columns, field.Name)
switch strings.ToLower(field.Index) { idxType := strings.ToLower(field.Index)
case "pk", "primary key": if idxType == "pk" || idxType == "primary key" {
pks = append(pks, field.Name) pks = append(pks, field.Name)
case "unique": } else if idxType == "unique" || idxType == "index" || idxType == "fulltext" {
keyName := fmt.Sprint("uk_", table.Name, "_", field.Name) group := field.IndexGroup
if field.IndexGroup != "" { if group == "" {
keyName = fmt.Sprint("uk_", table.Name, "_", field.IndexGroup) group = field.Name
} }
if keySetBy[keyName] != "" { groupKey := idxType + "_" + group
keySetFields[keyName] += " " + field.Name if _, exists := indexGroups[groupKey]; !exists {
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" { indexGroups[groupKey] = &indexGroupDef{Type: idxType, Fields: []string{}}
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", "+db.Quote(field.Name)+")", 1) }
} else if isPostgres { indexGroups[groupKey].Fields = append(indexGroups[groupKey].Fields, field.Name)
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)
// 阶段二:生成字段定义与索引定义
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"
} }
} else { keyName := fmt.Sprintf("%s_%s_%s", prefix, table.Name, joinedFields)
keySetFields[keyName] = field.Name
keySet := "" // 防超长截断处理 (最大 64留 4 个给安全余量,设为 60)
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" { if len(keyName) > 60 {
keySet = fmt.Sprintf("CREATE UNIQUE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name) hash := md5.Sum([]byte(keyName))
} else if isPostgres { hashStr := hex.EncodeToString(hash[:])[:6]
keySet = fmt.Sprintf("CREATE UNIQUE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name) keyName = keyName[:53] + "_" + hashStr
} else {
keySet = fmt.Sprintf("UNIQUE KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment)
} }
keySets = append(keySets, keySet)
keySetBy[keyName] = keySet keySetFields[keyName] = strings.Join(groupDef.Fields, " ")
}
case "fulltext": if idxType == "fulltext" {
ftsFields = append(ftsFields, field.Name) ftsFields = append(ftsFields, groupDef.Fields...)
keyName := fmt.Sprint("tk_", table.Name, "_", field.Name) keySet := ""
keySet := "" if isPostgres {
if isPostgres { // PostgreSQL 使用 simple 分词器
// 使用 simple 分词器,配合应用层的分词结果 keySet = fmt.Sprintf("CREATE INDEX IF NOT EXISTS \"%s\" ON \"%s\" USING GIN (to_tsvector('simple', \"%s\"))", keyName, table.Name, groupDef.Fields[0])
keySet = fmt.Sprintf("CREATE INDEX \"%s\" ON \"%s\" USING GIN (to_tsvector('simple', \"%s\"))", keyName, table.Name, field.Name) } else if !strings.HasPrefix(db.Config.Type, "sqlite") && db.Config.Type != "chai" {
} else if !strings.HasPrefix(db.Config.Type, "sqlite") && db.Config.Type != "chai" { keySet = fmt.Sprintf("FULLTEXT KEY "+db.Quote("%s")+" ("+db.Quotes(groupDef.Fields)+") COMMENT '%s'", keyName, field.Comment)
keySet = fmt.Sprintf("FULLTEXT KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment) }
} else { if keySet != "" {
// SQLite 使用 FTS5这里不生成普通索引 keySets = append(keySets, keySet)
keySet = "" keySetBy[keyName] = keySet
} }
if keySet != "" { } else if idxType == "unique" {
keySets = append(keySets, keySet) keySet := ""
keySetBy[keyName] = keySet if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
} keySet = fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
case "index": } else if isPostgres {
keyName := fmt.Sprint("ik_", table.Name, "_", field.Name) keySet = fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
if field.IndexGroup != "" { } else {
keyName = fmt.Sprint("ik_", table.Name, "_", field.IndexGroup) keySet = fmt.Sprintf("UNIQUE KEY "+db.Quote("%s")+" ("+db.Quotes(groupDef.Fields)+") COMMENT '%s'", keyName, field.Comment)
} }
if keySetBy[keyName] != "" { keySets = append(keySets, keySet)
keySetFields[keyName] += " " + field.Name keySetBy[keyName] = keySet
if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" { } else { // index
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", \""+field.Name+"\")", 1) keySet := ""
} else if isPostgres { if strings.HasPrefix(db.Config.Type, "sqlite") || db.Config.Type == "chai" {
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ")", ", \""+field.Name+"\")", 1) keySet = fmt.Sprintf("CREATE INDEX IF NOT EXISTS \"%s\" ON \"%s\" (%s)", keyName, table.Name, db.Quotes(groupDef.Fields))
} else { } else if isPostgres {
keySetBy[keyName] = strings.Replace(keySetBy[keyName], ") COMMENT", ", `"+field.Name+"`) COMMENT", 1) 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
} }
} else {
keySetFields[keyName] = field.Name
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)
} else if isPostgres {
keySet = fmt.Sprintf("CREATE INDEX \"%s\" ON \"%s\" (\"%s\")", keyName, table.Name, field.Name)
} else {
keySet = fmt.Sprintf("KEY "+db.Quote("%s")+" ("+db.Quote("%s")+") COMMENT '%s'", keyName, field.Name, field.Comment)
}
keySets = append(keySets, keySet)
keySetBy[keyName] = keySet
} }
} }
fieldSets = append(fieldSets, field.Desc) fieldSets = append(fieldSets, field.Desc)
@ -419,6 +443,7 @@ func (db *DB) CheckTable(table *TableStruct) error {
oldFieldList := make([]*tableFieldDesc, 0) oldFieldList := make([]*tableFieldDesc, 0)
oldFields := make(map[string]*tableFieldDesc) oldFields := make(map[string]*tableFieldDesc)
oldIndexes := make(map[string]string) oldIndexes := make(map[string]string)
oldUniqueMap := make(map[string]bool)
oldIndexInfos := make([]*tableKeyDesc, 0) oldIndexInfos := make([]*tableKeyDesc, 0)
oldComments := map[string]string{} oldComments := map[string]string{}
@ -452,6 +477,7 @@ func (db *DB) CheckTable(table *TableStruct) error {
return err return err
} }
for _, i := range tmpIndexes { for _, i := range tmpIndexes {
oldUniqueMap[i.Name] = i.Unique == 1
tmpIndexInfo := []struct { tmpIndexInfo := []struct {
Name string Name string
Seqno int 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 { if err := db.Query("PRAGMA index_info(" + db.Quote(i.Name) + ")").To(&tmpIndexInfo); err != nil {
return err return err
} }
if len(tmpIndexInfo) > 0 { for _, ii := range tmpIndexInfo {
oldIndexInfos = append(oldIndexInfos, &tableKeyDesc{ oldIndexInfos = append(oldIndexInfos, &tableKeyDesc{
Key_name: i.Name, 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 { for keyId, keySet := range keySetBy {
if oldIndexes[keyId] == "" || (!isPostgres && strings.ToLower(oldIndexes[keyId]) != strings.ToLower(keySetFields[keyId])) { 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) actions = append(actions, keySet)
} else { } else {
actions = append(actions, "ADD "+keySet) actions = append(actions, "ADD "+keySet)