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
|
# 变更记录 - @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
183
Schema.go
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user