Compare commits

..

No commits in common. "main" and "v1.5.2" have entirely different histories.
main ... v1.5.2

12 changed files with 193 additions and 172 deletions

View File

@ -1,10 +1,6 @@
# 变更记录 - @go/db # 变更记录 - @go/db
## [1.5.3] - 2026-06-08 ## [1.5.1] - 2026-06-04
- **新增**: `SetConfig(name, setting string)` 方法,支持动态配置数据库连接(不依赖配置文件),方便通过别名获取连接。
- **优化**: 重构配置加载逻辑,支持动态配置与 `config/db.yaml` 配置文件配置共存。
## [1.5.2] - 2026-06-05
- **架构重构: 两阶段 (Two-Pass) 索引命名生成**: - **架构重构: 两阶段 (Two-Pass) 索引命名生成**:
- 重构了 `CheckTable` 中的 Schema 同步逻辑,从“边解析边生成”改为两阶段处理。现在多列联合索引(无论是 Unique, Index 还是 Fulltext的名字会根据包含的**字段名称自动拼接生成**(如 `uk_user_name_phone`),替代了以前难以溯源的分组号(如 `uk_user_1`),极大提升了 DBA 在数据库维护时的可读性与直观性。 - 重构了 `CheckTable` 中的 Schema 同步逻辑,从“边解析边生成”改为两阶段处理。现在多列联合索引(无论是 Unique, Index 还是 Fulltext的名字会根据包含的**字段名称自动拼接生成**(如 `uk_user_name_phone`),替代了以前难以溯源的分组号(如 `uk_user_1`),极大提升了 DBA 在数据库维护时的可读性与直观性。
- **超长防断机制**: 当生成的索引名称超过 60 个字符时,自动进行截断,并在末尾追加原完整名称的 6 位 MD5 Hash完美规避了各种 RDBMS 对标识符 64 字符长度限制引发的建表错误。 - **超长防断机制**: 当生成的索引名称超过 60 个字符时,自动进行截断,并在末尾追加原完整名称的 6 位 MD5 Hash完美规避了各种 RDBMS 对标识符 64 字符长度限制引发的建表错误。

55
DB.go
View File

@ -412,20 +412,31 @@ func Begin() *Tx {
return d.Begin() return d.Begin()
} }
func SetConfig(name, setting string) { func getDB(name string, logger *log.Logger, useCache bool) *DB {
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 { if logger == nil {
logger = log.DefaultLogger logger = log.DefaultLogger
} }
if useCache {
dbInstancesLock.RLock()
oldConn := dbInstances[name]
dbInstancesLock.RUnlock()
if oldConn != nil {
return oldConn.CopyByLogger(logger)
}
}
var conf *Config
if strings.Contains(name, "://") {
conf = new(Config)
conf.logger = logger
conf.ConfigureBy(name)
} else {
dbConfigsLock.RLock()
n := len(dbConfigs)
dbConfigsLock.RUnlock()
if n == 0 {
once.Do(func() {
dbConfigs1 := make(map[string]*Config) dbConfigs1 := make(map[string]*Config)
if err := config.Load(&dbConfigs1, "db"); err == nil { if err := config.Load(&dbConfigs1, "db"); err == nil {
for k, v := range dbConfigs1 { for k, v := range dbConfigs1 {
@ -463,29 +474,7 @@ func loadConfigs(logger *log.Logger) {
logger.Error(err.Error()) logger.Error(err.Error())
} }
}) })
}
func getDB(name string, logger *log.Logger, useCache bool) *DB {
if logger == nil {
logger = log.DefaultLogger
} }
if useCache {
dbInstancesLock.RLock()
oldConn := dbInstances[name]
dbInstancesLock.RUnlock()
if oldConn != nil {
return oldConn.CopyByLogger(logger)
}
}
var conf *Config
if strings.Contains(name, "://") {
conf = new(Config)
conf.logger = logger
conf.ConfigureBy(name)
} else {
loadConfigs(logger)
dbConfigsLock.RLock() dbConfigsLock.RLock()
conf = dbConfigs[name] conf = dbConfigs[name]
dbConfigsLock.RUnlock() dbConfigsLock.RUnlock()

View File

@ -22,10 +22,8 @@ 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)。

22
debug/debug_sync.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"fmt"
"apigo.cc/go/db"
)
func main() {
dbPath := "debug.db"
dbInst := db.GetDB("sqlite://"+dbPath, nil)
schema := `
== System ==
_Table SD
id c10 PK
name v64 U
`
fmt.Println("First sync...")
dbInst.Sync(schema)
fmt.Println("\nSecond sync...")
dbInst.Sync(schema)
}

3
debug/go.mod Normal file
View File

@ -0,0 +1,3 @@
module debug
go 1.26.1

6
go.mod
View File

@ -16,7 +16,7 @@ require (
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.51.0 modernc.org/sqlite v1.50.0
) )
require ( require (
@ -29,7 +29,7 @@ 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.22 // indirect github.com/mattn/go-isatty v0.0.20 // 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.52.0 // indirect golang.org/x/crypto v0.52.0 // indirect
@ -37,7 +37,7 @@ require (
golang.org/x/sys v0.45.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.5 // indirect modernc.org/libc v1.72.0 // 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
) )

22
go.sum
View File

@ -51,7 +51,8 @@ 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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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=
@ -73,6 +74,7 @@ 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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
@ -85,24 +87,30 @@ 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.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
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.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
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.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U= modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
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.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
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.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U= modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
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=

View File

@ -12,69 +12,7 @@ import (
func init() { func init() {
jsmod.Register("db", map[string]any{ jsmod.Register("db", map[string]any{
// 入口:支持别名获取,不传则默认 "default" "get": func(ctx context.Context, name string) (*jsDB, error) {
"Get": func(ctx context.Context, name *string) (*jsDB, error) {
target := "default"
if name != nil {
target = *name
}
return getJSDB(ctx, target)
},
// 默认快捷调用 (面向 "default" 实例)
"Query": func(ctx context.Context, query string, args ...any) (*QueryResult, error) {
jd, err := getJSDB(ctx, "default")
if err != nil {
return nil, err
}
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 isReadOnly := false
// 安全模式下的动态判定 // 安全模式下的动态判定
@ -85,6 +23,7 @@ func getJSDB(ctx context.Context, name string) (*jsDB, error) {
if idx := strings.Index(path, "?"); idx != -1 { if idx := strings.Index(path, "?"); idx != -1 {
path = path[:idx] path = path[:idx]
} }
// 如果路径不在沙箱内VerifyPathForSafeMode 会直接返回 error阻止连接
_, err := file.VerifyPathForSafeMode(ctx, path) _, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -93,7 +32,7 @@ func getJSDB(ctx context.Context, name string) (*jsDB, error) {
} else { } else {
// 2. 远程数据库前缀校验 // 2. 远程数据库前缀校验
var allowedDSNs []string var allowedDSNs []string
cast.Convert(&allowedDSNs, jsmod.Get(ctx, "AllowedDSNs")) cast.Convert(&allowedDSNs, ctx.Value("AllowedDSNs"))
matched := false matched := false
for _, prefix := range allowedDSNs { for _, prefix := range allowedDSNs {
if strings.HasPrefix(name, prefix) { if strings.HasPrefix(name, prefix) {
@ -102,6 +41,7 @@ func getJSDB(ctx context.Context, name string) (*jsDB, error) {
} }
} }
if !matched { if !matched {
// 未命中白名单,降级为只读模式
isReadOnly = true isReadOnly = true
} }
} }
@ -112,6 +52,8 @@ func getJSDB(ctx context.Context, name string) (*jsDB, error) {
return nil, d.Error return nil, d.Error
} }
return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil
},
})
} }
type jsDB struct { type jsDB struct {
@ -179,10 +121,6 @@ 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
@ -244,3 +182,7 @@ 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) }

5
test_idx.sh Normal file
View File

@ -0,0 +1,5 @@
rm -f test_idx.db
sqlite3 test_idx.db "CREATE TABLE test (name TEXT);"
sqlite3 test_idx.db "CREATE INDEX idx_name ON test(name);"
sqlite3 test_idx.db "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name);"
echo "Result code: $?"

6
test_unique.sh Normal file
View File

@ -0,0 +1,6 @@
rm -f test_unique.db
sqlite3 test_unique.db "CREATE TABLE test (name TEXT);"
sqlite3 test_unique.db "INSERT INTO test (name) VALUES ('a');"
sqlite3 test_unique.db "INSERT INTO test (name) VALUES ('b');"
sqlite3 test_unique.db "CREATE UNIQUE INDEX uk_test_name ON test(name);"
echo "Result code: $?"

6
test_unique_dup.sh Normal file
View File

@ -0,0 +1,6 @@
rm -f test_unique.db
sqlite3 test_unique.db "CREATE TABLE test (name TEXT);"
sqlite3 test_unique.db "INSERT INTO test (name) VALUES ('a');"
sqlite3 test_unique.db "INSERT INTO test (name) VALUES ('a');"
sqlite3 test_unique.db "CREATE UNIQUE INDEX uk_test_name ON test(name);"
echo "Result code: $?"

46
unique_race_test.go Normal file
View File

@ -0,0 +1,46 @@
package db
import (
"fmt"
"os"
"testing"
)
func TestCheckTable_DuplicateUnique(t *testing.T) {
dbPath := "test_unique_race.db"
_ = os.Remove(dbPath)
defer os.Remove(dbPath)
dbInst := GetDB("sqlite://"+dbPath, nil)
schema := `
== System ==
_Table SD
id c10 PK
name v64 U
`
// First sync
err := dbInst.Sync(schema)
if err != nil {
t.Fatalf("First sync failed: %v", err)
}
// Insert duplicates to ensure recreation fails
dbInst.Exec("INSERT INTO _Table (id, name) VALUES ('1', 'dup')")
dbInst.Exec("INSERT INTO _Table (id, name) VALUES ('2', 'dup')") // Will fail if unique constraint works, but wait, the unique index is already there, so we can't insert duplicates!
// Wait, we CAN'T insert duplicates because the first sync created the index.
// But in the user's log, there ARE duplicates? Or maybe there are NO duplicates, but the engine just complains when it recreates the index on existing data?
// Ah, if there are NO duplicates, CREATE UNIQUE INDEX will SUCCEED.
// So why did the user get UNIQUE constraint failed?
// BECAUSE THERE WERE DUPLICATES!
// Why would there be duplicates in _Table?
// Let's just do the second sync and see if it tries to execute CREATE UNIQUE INDEX.
// Print statements will be inside Schema.go for a moment.
// Second sync (simulate restart)
err = dbInst.Sync(schema)
if err != nil {
t.Fatalf("Second sync failed: %v", err)
}
}