Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1312095ce7 | ||
|
|
63cd9d392a | ||
|
|
38e520c300 | ||
|
|
39eb72c450 | ||
|
|
41e2bac5e2 | ||
|
|
74b9bfe511 |
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,5 +1,20 @@
|
|||||||
# 变更记录 - @go/db
|
# 变更记录 - @go/db
|
||||||
|
|
||||||
|
## [1.5.3] - 2026-06-08
|
||||||
|
- **新增**: `SetConfig(name, setting string)` 方法,支持动态配置数据库连接(不依赖配置文件),方便通过别名获取连接。
|
||||||
|
- **优化**: 重构配置加载逻辑,支持动态配置与 `config/db.yaml` 配置文件配置共存。
|
||||||
|
|
||||||
|
## [1.5.2] - 2026-06-05
|
||||||
|
- **架构重构: 两阶段 (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。
|
||||||
|
|||||||
97
DB.go
97
DB.go
@ -412,6 +412,59 @@ func Begin() *Tx {
|
|||||||
return d.Begin()
|
return d.Begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetConfig(name, setting string) {
|
||||||
|
loadConfigs(nil)
|
||||||
|
conf := new(Config)
|
||||||
|
conf.ConfigureBy(setting)
|
||||||
|
dbConfigsLock.Lock()
|
||||||
|
dbConfigs[name] = conf
|
||||||
|
dbConfigsLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfigs(logger *log.Logger) {
|
||||||
|
once.Do(func() {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.DefaultLogger
|
||||||
|
}
|
||||||
|
dbConfigs1 := make(map[string]*Config)
|
||||||
|
if err := config.Load(&dbConfigs1, "db"); err == nil {
|
||||||
|
for k, v := range dbConfigs1 {
|
||||||
|
if v.Host != "" {
|
||||||
|
dbConfigsLock.Lock()
|
||||||
|
dbConfigs[k] = v
|
||||||
|
dbConfigsLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
dbConfigs2 := make(map[string]string)
|
||||||
|
if err := config.Load(&dbConfigs2, "db"); err == nil {
|
||||||
|
for k, v := range dbConfigs2 {
|
||||||
|
if strings.Contains(v, "://") {
|
||||||
|
v2 := new(Config)
|
||||||
|
v2.ConfigureBy(v)
|
||||||
|
if v2.Host != "" {
|
||||||
|
v2.logger = logger
|
||||||
|
dbConfigsLock.Lock()
|
||||||
|
dbConfigs[k] = v2
|
||||||
|
dbConfigsLock.Unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbConfigsLock.Lock()
|
||||||
|
v2 := dbConfigs[v]
|
||||||
|
if v2 != nil && v2.Host != "" {
|
||||||
|
dbConfigs[k] = v2
|
||||||
|
}
|
||||||
|
dbConfigsLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func getDB(name string, logger *log.Logger, useCache bool) *DB {
|
func getDB(name string, logger *log.Logger, useCache bool) *DB {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.DefaultLogger
|
logger = log.DefaultLogger
|
||||||
@ -432,49 +485,7 @@ func getDB(name string, logger *log.Logger, useCache bool) *DB {
|
|||||||
conf.logger = logger
|
conf.logger = logger
|
||||||
conf.ConfigureBy(name)
|
conf.ConfigureBy(name)
|
||||||
} else {
|
} else {
|
||||||
dbConfigsLock.RLock()
|
loadConfigs(logger)
|
||||||
n := len(dbConfigs)
|
|
||||||
dbConfigsLock.RUnlock()
|
|
||||||
if n == 0 {
|
|
||||||
once.Do(func() {
|
|
||||||
dbConfigs1 := make(map[string]*Config)
|
|
||||||
if err := config.Load(&dbConfigs1, "db"); err == nil {
|
|
||||||
for k, v := range dbConfigs1 {
|
|
||||||
if v.Host != "" {
|
|
||||||
dbConfigsLock.Lock()
|
|
||||||
dbConfigs[k] = v
|
|
||||||
dbConfigsLock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
}
|
|
||||||
dbConfigs2 := make(map[string]string)
|
|
||||||
if err := config.Load(&dbConfigs2, "db"); err == nil {
|
|
||||||
for k, v := range dbConfigs2 {
|
|
||||||
if strings.Contains(v, "://") {
|
|
||||||
v2 := new(Config)
|
|
||||||
v2.ConfigureBy(v)
|
|
||||||
if v2.Host != "" {
|
|
||||||
v2.logger = logger
|
|
||||||
dbConfigsLock.Lock()
|
|
||||||
dbConfigs[k] = v2
|
|
||||||
dbConfigsLock.Unlock()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbConfigsLock.Lock()
|
|
||||||
v2 := dbConfigs[v]
|
|
||||||
if v2 != nil && v2.Host != "" {
|
|
||||||
dbConfigs[k] = v2
|
|
||||||
}
|
|
||||||
dbConfigsLock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
dbConfigsLock.RLock()
|
dbConfigsLock.RLock()
|
||||||
conf = dbConfigs[name]
|
conf = dbConfigs[name]
|
||||||
dbConfigsLock.RUnlock()
|
dbConfigsLock.RUnlock()
|
||||||
|
|||||||
@ -22,8 +22,10 @@ go get apigo.cc/go/db
|
|||||||
## 🛠 API 指南
|
## 🛠 API 指南
|
||||||
|
|
||||||
### 1. 核心方法
|
### 1. 核心方法
|
||||||
|
- **`SetConfig(name, setting string)`**
|
||||||
|
- 动态设置数据库配置(不依赖配置文件),可通过别名获取连接。
|
||||||
- **`GetDB(name string, logger *log.Logger) *DB`**
|
- **`GetDB(name string, logger *log.Logger) *DB`**
|
||||||
- 获取数据库连接实例。`name` 可以是 `db.json` 中的配置名,也可以是标准 DSN(如 `mysql://user:pwd@host:port/db` 或 `sqlite://test.db`)。
|
- 获取数据库连接实例。`name` 可以是别名、`db.json` 中的配置名,也可以是标准 DSN(如 `mysql://user:pwd@host:port/db` 或 `sqlite://test.db`)。
|
||||||
- **`Sync(schema string) error`**
|
- **`Sync(schema string) error`**
|
||||||
- 解析 DSL 并同步数据库表结构。用于创建表(包括 `_deleted` 表)和索引。详见 [架构 DSL 指南](./DSL.md)。
|
- 解析 DSL 并同步数据库表结构。用于创建表(包括 `_deleted` 表)和索引。详见 [架构 DSL 指南](./DSL.md)。
|
||||||
|
|
||||||
|
|||||||
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)
|
||||||
|
|||||||
14
go.mod
14
go.mod
@ -4,19 +4,19 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.5.0
|
apigo.cc/go/cast v1.5.0
|
||||||
apigo.cc/go/config v1.5.0
|
apigo.cc/go/config v1.5.1
|
||||||
apigo.cc/go/crypto v1.5.0
|
apigo.cc/go/crypto v1.5.0
|
||||||
apigo.cc/go/file v1.5.0
|
apigo.cc/go/file v1.5.0
|
||||||
apigo.cc/go/id v1.5.0
|
apigo.cc/go/id v1.5.0
|
||||||
apigo.cc/go/jsmod v1.5.0
|
apigo.cc/go/jsmod v1.5.0
|
||||||
apigo.cc/go/log v1.5.0
|
apigo.cc/go/log v1.5.4
|
||||||
apigo.cc/go/redis v1.5.0
|
apigo.cc/go/redis v1.5.0
|
||||||
apigo.cc/go/safe v1.5.0
|
apigo.cc/go/safe v1.5.0
|
||||||
apigo.cc/go/shell v1.5.0
|
apigo.cc/go/shell v1.5.0
|
||||||
github.com/go-sql-driver/mysql v1.10.0
|
github.com/go-sql-driver/mysql v1.10.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
modernc.org/sqlite v1.50.0
|
modernc.org/sqlite v1.51.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -29,15 +29,15 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.72.0 // indirect
|
modernc.org/libc v1.72.5 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
38
go.sum
38
go.sum
@ -1,7 +1,7 @@
|
|||||||
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||||
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||||
apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
|
apigo.cc/go/config v1.5.1 h1:rpj7oCzlsDV3f2/YK3Pb+CHbfr2DL5Vyyv6VNkobJP4=
|
||||||
apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
|
apigo.cc/go/config v1.5.1/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
|
||||||
apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI=
|
apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI=
|
||||||
apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE=
|
apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE=
|
||||||
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
||||||
@ -12,8 +12,8 @@ apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
|
|||||||
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
|
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
|
||||||
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||||
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
apigo.cc/go/log v1.5.0 h1:kQuLLtbt33mEuc/xJVcy8NODXkso/QKSZWNclKrSpsI=
|
apigo.cc/go/log v1.5.4 h1:LNyU4v09gfcnZOY53ctnXoKzo45FHoEcPR33lk6PBaY=
|
||||||
apigo.cc/go/log v1.5.0/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
|
apigo.cc/go/log v1.5.4/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
|
||||||
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
||||||
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
||||||
apigo.cc/go/redis v1.5.0 h1:VXNDqzKj87BchF7ubDEH+T6lp8NrjeK0izU4ooo7u1A=
|
apigo.cc/go/redis v1.5.0 h1:VXNDqzKj87BchF7ubDEH+T6lp8NrjeK0izU4ooo7u1A=
|
||||||
@ -51,8 +51,7 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
@ -68,15 +67,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
@ -87,30 +85,24 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo=
|
||||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
|
||||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U=
|
||||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
||||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
140
js_export.go
140
js_export.go
@ -12,50 +12,108 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
jsmod.Register("db", map[string]any{
|
jsmod.Register("db", map[string]any{
|
||||||
"get": func(ctx context.Context, name string) (*jsDB, error) {
|
// 入口:支持别名获取,不传则默认 "default"
|
||||||
isReadOnly := false
|
"Get": func(ctx context.Context, name *string) (*jsDB, error) {
|
||||||
|
target := "default"
|
||||||
// 安全模式下的动态判定
|
if name != nil {
|
||||||
if jsmod.IsSafeMode(ctx) {
|
target = *name
|
||||||
if strings.HasPrefix(name, "sqlite://") {
|
|
||||||
// 1. SQLite 路径校验
|
|
||||||
path := strings.TrimPrefix(name, "sqlite://")
|
|
||||||
if idx := strings.Index(path, "?"); idx != -1 {
|
|
||||||
path = path[:idx]
|
|
||||||
}
|
|
||||||
// 如果路径不在沙箱内,VerifyPathForSafeMode 会直接返回 error,阻止连接
|
|
||||||
_, err := file.VerifyPathForSafeMode(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isReadOnly = false
|
|
||||||
} else {
|
|
||||||
// 2. 远程数据库前缀校验
|
|
||||||
var allowedDSNs []string
|
|
||||||
cast.Convert(&allowedDSNs, ctx.Value("AllowedDSNs"))
|
|
||||||
matched := false
|
|
||||||
for _, prefix := range allowedDSNs {
|
|
||||||
if strings.HasPrefix(name, prefix) {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
// 未命中白名单,降级为只读模式
|
|
||||||
isReadOnly = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return getJSDB(ctx, target)
|
||||||
|
},
|
||||||
|
|
||||||
d := GetDB(name, nil)
|
// 默认快捷调用 (面向 "default" 实例)
|
||||||
if d.Error != nil {
|
"Query": func(ctx context.Context, query string, args ...any) (*QueryResult, error) {
|
||||||
return nil, d.Error
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil
|
return jd.Query(query, args...), nil
|
||||||
|
},
|
||||||
|
"Exec": func(ctx context.Context, query string, args ...any) (*ExecResult, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Exec(query, args...), nil
|
||||||
|
},
|
||||||
|
"Insert": func(ctx context.Context, table string, data any) (*ExecResult, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Insert(table, data), nil
|
||||||
|
},
|
||||||
|
"Update": func(ctx context.Context, table string, data any, conditions string, args ...any) (*ExecResult, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Update(table, data, conditions, args...), nil
|
||||||
|
},
|
||||||
|
"Delete": func(ctx context.Context, table string, conditions string, args ...any) (*ExecResult, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Delete(table, conditions, args...), nil
|
||||||
|
},
|
||||||
|
"Replace": func(ctx context.Context, table string, data any) (*ExecResult, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Replace(table, data), nil
|
||||||
|
},
|
||||||
|
"Begin": func(ctx context.Context) (*jsTx, error) {
|
||||||
|
jd, err := getJSDB(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jd.Begin()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getJSDB(ctx context.Context, name string) (*jsDB, error) {
|
||||||
|
isReadOnly := false
|
||||||
|
|
||||||
|
// 安全模式下的动态判定
|
||||||
|
if jsmod.IsSafeMode(ctx) {
|
||||||
|
if strings.HasPrefix(name, "sqlite://") {
|
||||||
|
// 1. SQLite 路径校验
|
||||||
|
path := strings.TrimPrefix(name, "sqlite://")
|
||||||
|
if idx := strings.Index(path, "?"); idx != -1 {
|
||||||
|
path = path[:idx]
|
||||||
|
}
|
||||||
|
_, err := file.VerifyPathForSafeMode(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
isReadOnly = false
|
||||||
|
} else {
|
||||||
|
// 2. 远程数据库前缀校验
|
||||||
|
var allowedDSNs []string
|
||||||
|
cast.Convert(&allowedDSNs, jsmod.Get(ctx, "AllowedDSNs"))
|
||||||
|
matched := false
|
||||||
|
for _, prefix := range allowedDSNs {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
isReadOnly = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d := GetDB(name, nil)
|
||||||
|
if d.Error != nil {
|
||||||
|
return nil, d.Error
|
||||||
|
}
|
||||||
|
return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type jsDB struct {
|
type jsDB struct {
|
||||||
db *DB
|
db *DB
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@ -121,6 +179,10 @@ func (jd *jsDB) Begin() (*jsTx, error) {
|
|||||||
return &jsTx{tx: tx, ctx: jd.ctx, isReadOnly: jd.isReadOnly}, nil
|
return &jsTx{tx: tx, ctx: jd.ctx, isReadOnly: jd.isReadOnly}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
func (jd *jsDB) InKeys(numArgs int) string { return jd.db.InKeys(numArgs) }
|
||||||
|
func (jd *jsDB) Quote(text string) string { return jd.db.Quote(text) }
|
||||||
|
|
||||||
// jsTx wraps *Tx for JS environment
|
// jsTx wraps *Tx for JS environment
|
||||||
type jsTx struct {
|
type jsTx struct {
|
||||||
tx *Tx
|
tx *Tx
|
||||||
@ -182,7 +244,3 @@ func (jt *jsTx) Commit() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (jt *jsTx) Rollback() error { return jt.tx.Rollback() }
|
func (jt *jsTx) Rollback() error { return jt.tx.Rollback() }
|
||||||
|
|
||||||
// Metadata
|
|
||||||
func (jd *jsDB) InKeys(numArgs int) string { return jd.db.InKeys(numArgs) }
|
|
||||||
func (jd *jsDB) Quote(text string) string { return jd.db.Quote(text) }
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user