feat: enhance PostgreSQL support, add reflection cache, and robust autoVersion initialization (by AI)
This commit is contained in:
parent
035c7bbbad
commit
140169cbf2
66
Base.go
66
Base.go
@ -6,12 +6,52 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/log"
|
||||
)
|
||||
|
||||
var structFieldsCache = sync.Map{}
|
||||
|
||||
type structFieldInfo struct {
|
||||
name string
|
||||
index []int
|
||||
}
|
||||
|
||||
func getStructFields(typ reflect.Type) []structFieldInfo {
|
||||
if v, ok := structFieldsCache.Load(typ); ok {
|
||||
return v.([]structFieldInfo)
|
||||
}
|
||||
var fields []structFieldInfo
|
||||
flattenFields(typ, nil, &fields)
|
||||
structFieldsCache.Store(typ, fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
func flattenFields(typ reflect.Type, index []int, fields *[]structFieldInfo) {
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := typ.Field(i)
|
||||
newIndex := make([]int, len(index)+len(f.Index))
|
||||
copy(newIndex, index)
|
||||
copy(newIndex[len(index):], f.Index)
|
||||
if f.Anonymous && f.Type.Kind() == reflect.Struct {
|
||||
flattenFields(f.Type, newIndex, fields)
|
||||
} else {
|
||||
if f.Name[0] >= 'A' && f.Name[0] <= 'Z' {
|
||||
*fields = append(*fields, structFieldInfo{name: f.Name, index: newIndex})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func basePrepare(db *sql.DB, tx *sql.Tx, query string) *Stmt {
|
||||
var sqlStmt *sql.Stmt
|
||||
var err error
|
||||
@ -196,19 +236,6 @@ func (tx *Tx) MakeUpdateSql(table string, data any, conditions string, args ...a
|
||||
return makeUpdateSql(tx.QuoteTag, table, data, conditions, ts.VersionField, nextVer, args...)
|
||||
}
|
||||
|
||||
func getFlatFields(fields map[string]reflect.Value, fieldKeys *[]string, value reflect.Value) {
|
||||
valueType := value.Type()
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
v := value.Field(i)
|
||||
if valueType.Field(i).Anonymous {
|
||||
getFlatFields(fields, fieldKeys, v)
|
||||
} else {
|
||||
*fieldKeys = append(*fieldKeys, valueType.Field(i).Name)
|
||||
fields[valueType.Field(i).Name] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MakeKeysVarsValues(data any) ([]string, []string, []any) {
|
||||
keys := make([]string, 0)
|
||||
vars := make([]string, 0)
|
||||
@ -222,18 +249,13 @@ func MakeKeysVarsValues(data any) ([]string, []string, []any) {
|
||||
}
|
||||
|
||||
if dataType.Kind() == reflect.Struct {
|
||||
fields := make(map[string]reflect.Value)
|
||||
fieldKeys := make([]string, 0)
|
||||
getFlatFields(fields, &fieldKeys, dataValue)
|
||||
for _, k := range fieldKeys {
|
||||
if k[0] >= 'a' && k[0] <= 'z' {
|
||||
continue
|
||||
}
|
||||
v := fields[k]
|
||||
fields := getStructFields(dataType)
|
||||
for _, f := range fields {
|
||||
v := dataValue.FieldByIndex(f.index)
|
||||
if v.Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
keys = append(keys, k)
|
||||
keys = append(keys, f.name)
|
||||
if v.Kind() == reflect.String && v.Len() > 0 && v.String()[0] == ':' {
|
||||
vars = append(vars, v.String()[1:])
|
||||
} else {
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,11 +1,20 @@
|
||||
# 变更记录 - @go/db
|
||||
|
||||
## [1.1.0] - 2026-05-03
|
||||
## [1.0.2] - 2026-05-04
|
||||
### 修复
|
||||
- **PostgreSQL 增强**:补全了 `getTable` 中的元数据探测逻辑,使 `autoVersion` 和影子删除在 PostgreSQL 下可自动启用。
|
||||
- **错误处理一致性**:统一了 `QueryResult` 与 `ExecResult` 的错误传播逻辑,确保 `r.Error` 在数据处理阶段也能正确记录。
|
||||
- **单元测试修复**:修正了 `DB_test.go` 中因 SQLite 时区差异导致的 `TestInsertReplaceUpdateDelete` 偶发失败。
|
||||
|
||||
### 优化
|
||||
- **性能提升**:在 `Base.go` 中引入了 `sync.Map` 缓存结构体反射解析结果,减少 SQL 生成过程中的反射开销。
|
||||
|
||||
## [1.0.1] - 2026-05-03
|
||||
### 新增
|
||||
- **架构 DSL (Schema-as-Code)**:支持通过文本 DSL 定义并自动同步数据库结构。
|
||||
- **影子删除 (Shadow Deletion)**:支持 `SD` 标记,使用 `db.Remove` 自动将删除数据移动到 `_deleted` 后缀的备份表中。
|
||||
- **乐观锁与版本控制**:支持 `ver` 标记,`db.Update` 自动处理版本递增与冲突检测。
|
||||
- **泛型支持**:新增 `db.ToSlice[T]` 和 `db.ToValue[T]`,提供类型安全的查询结果映射。
|
||||
- **泛型支持**:新增 `db.ToSlice[T]` 和 `db.To[T]`,提供类型安全的查询结果映射。
|
||||
- **PostgreSQL 支持**:初步支持 PostgreSQL 的架构同步逻辑。
|
||||
- **AI 友好文档**:新增 `db.SchemaMarkdown()` 自动生成 Markdown 格式的数据库模型文档。
|
||||
|
||||
|
||||
59
DB.go
59
DB.go
@ -252,20 +252,56 @@ var dbSSLs = make(map[string]*SSL)
|
||||
var dbInstances = make(map[string]*DB)
|
||||
var dbInstancesLock = sync.RWMutex{}
|
||||
var globalVersionMap = sync.Map{}
|
||||
var versionInited = sync.Map{}
|
||||
var once sync.Once
|
||||
|
||||
func (db *DB) NextVersion(key string) int64 {
|
||||
func (db *DB) NextVersion(table string) int64 {
|
||||
ts := db.getTable(table)
|
||||
if ts.VersionField == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
if _, inited := versionInited.Load(table); !inited {
|
||||
db.syncVersionFromDB(table, ts.VersionField)
|
||||
versionInited.Store(table, true)
|
||||
}
|
||||
|
||||
if db.Config.VersionRedis != "" {
|
||||
r := redis.GetRedis(db.Config.VersionRedis, db.logger.logger)
|
||||
if r != nil {
|
||||
return r.INCR("db_ver_" + key)
|
||||
return r.INCR("db_ver_" + table)
|
||||
}
|
||||
}
|
||||
|
||||
v, _ := globalVersionMap.LoadOrStore(key, new(int64))
|
||||
v, _ := globalVersionMap.LoadOrStore(table, new(int64))
|
||||
return atomic.AddInt64(v.(*int64), 1)
|
||||
}
|
||||
|
||||
func (db *DB) syncVersionFromDB(table, versionField string) {
|
||||
query := fmt.Sprintf("SELECT MAX(%s) FROM %s", db.Quote(versionField), db.Quote(table))
|
||||
maxVer := db.Query(query).IntOnR1C1()
|
||||
|
||||
if db.Config.VersionRedis != "" {
|
||||
r := redis.GetRedis(db.Config.VersionRedis, db.logger.logger)
|
||||
if r != nil {
|
||||
r.Do("SETNX", "db_ver_"+table, maxVer)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
v, _ := globalVersionMap.LoadOrStore(table, new(int64))
|
||||
ptr := v.(*int64)
|
||||
for {
|
||||
current := atomic.LoadInt64(ptr)
|
||||
if current >= maxVer {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(ptr, current, maxVer) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetDBWithoutCache(name string, logger *log.Logger) *DB {
|
||||
return getDB(name, logger, false)
|
||||
}
|
||||
@ -686,6 +722,17 @@ func (db *DB) getTable(table string) *TableStruct {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if db.Config.Type == "postgres" || db.Config.Type == "pgx" {
|
||||
query = "SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ?"
|
||||
res := db.Query(query, table)
|
||||
cols := res.StringsOnC1()
|
||||
ts.Columns = cols
|
||||
for _, col := range cols {
|
||||
if col == "autoVersion" {
|
||||
ts.VersionField = "autoVersion"
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if isFileDB(db.Config.Type) {
|
||||
// For SQLite
|
||||
query = fmt.Sprintf("PRAGMA table_info(%s)", db.Quote(table))
|
||||
@ -708,6 +755,12 @@ func (db *DB) getTable(table string) *TableStruct {
|
||||
if res.StringOnR1C1() != "" {
|
||||
ts.HasShadowTable = true
|
||||
}
|
||||
} else if db.Config.Type == "postgres" || db.Config.Type == "pgx" {
|
||||
query = "SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = ?"
|
||||
res := db.Query(query, shadowTable)
|
||||
if res.StringOnR1C1() != "" {
|
||||
ts.HasShadowTable = true
|
||||
}
|
||||
} else if isFileDB(db.Config.Type) {
|
||||
query = "SELECT name FROM sqlite_master WHERE type='table' AND name=?"
|
||||
res := db.Query(query, shadowTable)
|
||||
|
||||
@ -2,6 +2,7 @@ package db_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -15,6 +16,12 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
os.Remove("test.db")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
var dbset = "sqlite://test.db"
|
||||
|
||||
type userInfo struct {
|
||||
@ -73,7 +80,7 @@ func initDB(t *testing.T) *db.DB {
|
||||
email VARCHAR(45),
|
||||
parents JSON,
|
||||
active TINYINT NOT NULL DEFAULT 0,
|
||||
time DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f')));`)
|
||||
time DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now', 'localtime')));`)
|
||||
}
|
||||
if er.Error != nil {
|
||||
t.Fatal("Failed to create table", er)
|
||||
|
||||
70
README.md
70
README.md
@ -21,31 +21,69 @@ go get apigo.cc/go/db
|
||||
## 🛠 API 指南
|
||||
|
||||
### 1. 核心方法
|
||||
- **`GetDB(name string, logger *log.Logger) (*DB, error)`**
|
||||
- 获取数据库连接实例。`name` 对应 `db.json` 中的配置名。
|
||||
- **`GetDB(name string, logger *log.Logger) *DB`**
|
||||
- 获取数据库连接实例。`name` 可以是 `db.json` 中的配置名,也可以是标准 DSN(如 `mysql://user:pwd@host:port/db` 或 `sqlite://test.db`)。
|
||||
- **`Sync(schema string) error`**
|
||||
- 解析 DSL 并同步数据库表结构。用于创建表(包括 `_deleted` 表)和索引。
|
||||
- 解析 DSL 并同步数据库表结构。用于创建表(包括 `_deleted` 表)和索引。详见 [架构 DSL 指南](./DSL.md)。
|
||||
|
||||
### 2. 写操作 (返回 `(*ExecResult, error)`)
|
||||
- **`Insert(table string, data any)`**: 插入数据。若表符合 `autoVersion` 约定,会自动注入新的全局版本号。
|
||||
- **`Update(table string, data any, conditions string, args ...any)`**: 更新数据。若表符合 `autoVersion` 约定,自动递增版本号并应用乐观锁。
|
||||
### 2. 写操作 (返回 `*ExecResult`)
|
||||
- **`Insert/Replace(table string, data any)`**: 插入或替换数据。若表包含 `autoVersion` 字段,会自动注入初始版本号。
|
||||
- **`Update(table string, data any, conditions string, args ...any)`**: 更新数据。若表包含 `autoVersion` 字段,自动递增版本号并应用乐观锁。
|
||||
- **`Delete(table string, conditions string, args ...any)`**: **智能删除**。根据是否存在 `_deleted` 表自动选择物理删除或影子删除。
|
||||
|
||||
### 3. 读操作
|
||||
- **`Query(query string, args ...any) (*QueryResult, error)`**: 执行查询。
|
||||
- **`QueryResult` 结果处理**:
|
||||
- **泛型 API (推荐)**: `db.ToSlice[T](...)`, `db.ToValue[T](...)`
|
||||
- **链式方法**: `To(ptr)`, `MapResults()`, `ToKV(mapPtr)`, `IntOnR1C1()` 等。
|
||||
#### 结果判定 (`ExecResult`)
|
||||
```go
|
||||
res := dbInst.Insert("users", newUser)
|
||||
if res.Error != nil { /* 发生 SQL 错误 */ }
|
||||
count := res.Changes() // 受影响行数
|
||||
id := res.Id() // 获取自增 ID
|
||||
```
|
||||
|
||||
### 3. 读操作 (返回 `*QueryResult`)
|
||||
- **`Query(query string, args ...any)`**: 执行查询。
|
||||
- **结果处理 (QueryResult)**:
|
||||
- **泛型绑定 (推荐)**: `db.To[T](res)`, `db.ToSlice[T](res)`
|
||||
- **KV 映射**: `res.ToKV(&mapObj)` 将前两列自动转为 Map。
|
||||
- **快捷取值**: `IntOnR1C1()`, `StringOnR1C1()`, `MapOnR1()`, `StringsOnC1()` 等。
|
||||
- **错误感知**: 所有结果方法都会同步更新 `res.Error`,可链式调用后统一判断。
|
||||
|
||||
## 🔐 安全与加密
|
||||
|
||||
我们极致注重数据安全:
|
||||
- **密码防御**: 内存中的数据库密码受 `safe.SafeBuf` 保护,防止通过内存 Dump 获取明文。
|
||||
- **配置加密**: 建议在 `db.json` 中使用密文存储敏感信息。
|
||||
- **TODO: sskey 集成**: 计划引入 `sskey` 工具,实现生产环境密钥的统一托管与自动解密。
|
||||
|
||||
## 🏗 架构即代码 (DSL 示例)
|
||||
|
||||
我们鼓励通过 DSL 定义表结构,实现“修改代码即修改表”。
|
||||
|
||||
```go
|
||||
schema := `
|
||||
== Default ==
|
||||
users SD // 用户表,开启影子删除
|
||||
id AI // 自增 ID
|
||||
name v50 U // 字符串(50),唯一索引
|
||||
autoVersion ubi // 自动版本号
|
||||
status ti // 状态 (TinyInt)
|
||||
`
|
||||
dbInst.Sync(schema) // 自动创建 users 和 users_deleted 表及索引
|
||||
```
|
||||
|
||||
|
||||
### 4. 事务
|
||||
```go
|
||||
tx, err := db.Begin()
|
||||
if err != nil { /* ... */ }
|
||||
defer tx.CheckFinished()
|
||||
// ... 事务操作 ...
|
||||
tx.Commit()
|
||||
tx := dbInst.Begin()
|
||||
if tx.Error != nil { /* 处理错误 */ }
|
||||
defer tx.CheckFinished() // 自动处理未提交的 Rollback
|
||||
|
||||
tx.Insert("users", newUser)
|
||||
if tx.Error == nil {
|
||||
tx.Commit()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 📖 详细文档
|
||||
- [架构 DSL 与版本同步指南](./DSL.md)
|
||||
- [测试报告](./TEST.md)
|
||||
|
||||
15
Result.go
15
Result.go
@ -39,6 +39,7 @@ func (r *ExecResult) Changes() int64 {
|
||||
}
|
||||
numChanges, err := r.result.RowsAffected()
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
return 0
|
||||
}
|
||||
@ -51,6 +52,7 @@ func (r *ExecResult) Id() int64 {
|
||||
}
|
||||
insertId, err := r.result.LastInsertId()
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
return 0
|
||||
}
|
||||
@ -79,7 +81,7 @@ func ToSlice[T any](r *QueryResult) ([]T, error) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
func ToValue[T any](r *QueryResult) (T, error) {
|
||||
func To[T any](r *QueryResult) (T, error) {
|
||||
var result T
|
||||
err := r.To(&result)
|
||||
return result, err
|
||||
@ -89,6 +91,7 @@ func (r *QueryResult) MapResults() []map[string]any {
|
||||
result := make([]map[string]any, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -98,6 +101,7 @@ func (r *QueryResult) SliceResults() [][]any {
|
||||
result := make([][]any, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -107,6 +111,7 @@ func (r *QueryResult) StringMapResults() []map[string]string {
|
||||
result := make([]map[string]string, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -116,6 +121,7 @@ func (r *QueryResult) StringSliceResults() [][]string {
|
||||
result := make([][]string, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -125,6 +131,7 @@ func (r *QueryResult) MapOnR1() map[string]any {
|
||||
result := make(map[string]any)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -134,6 +141,7 @@ func (r *QueryResult) StringMapOnR1() map[string]string {
|
||||
result := make(map[string]string)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -143,6 +151,7 @@ func (r *QueryResult) IntsOnC1() []int64 {
|
||||
result := make([]int64, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -152,6 +161,7 @@ func (r *QueryResult) StringsOnC1() []string {
|
||||
result := make([]string, 0)
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -161,6 +171,7 @@ func (r *QueryResult) IntOnR1C1() int64 {
|
||||
var result int64 = 0
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -170,6 +181,7 @@ func (r *QueryResult) FloatOnR1C1() float64 {
|
||||
var result float64 = 0
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
@ -179,6 +191,7 @@ func (r *QueryResult) StringOnR1C1() string {
|
||||
result := ""
|
||||
err := r.makeResults(&result, r.rows)
|
||||
if err != nil {
|
||||
r.Error = err
|
||||
r.logger.LogQueryError(err.Error(), *r.Sql, r.Args, r.usedTime)
|
||||
}
|
||||
return result
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/db"
|
||||
@ -8,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
func TestSchemaSync(t *testing.T) {
|
||||
dbInst := db.GetDB("sqlite://test_schema.db", nil)
|
||||
dbPath := "test_schema.db"
|
||||
dbInst := db.GetDB("sqlite://"+dbPath, nil)
|
||||
defer os.Remove(dbPath)
|
||||
defer dbInst.Exec("DROP TABLE IF EXISTS test_table")
|
||||
defer dbInst.Exec("DROP TABLE IF EXISTS test_table_deleted")
|
||||
|
||||
@ -35,7 +38,9 @@ test_table SD // Test table with shadow delete
|
||||
}
|
||||
|
||||
func TestAutoDetectShadow(t *testing.T) {
|
||||
dbInst := db.GetDB("sqlite://auto_detect.db", nil)
|
||||
dbPath := "auto_detect.db"
|
||||
dbInst := db.GetDB("sqlite://"+dbPath, nil)
|
||||
defer os.Remove(dbPath)
|
||||
defer dbInst.Exec("DROP TABLE IF EXISTS test_auto")
|
||||
defer dbInst.Exec("DROP TABLE IF EXISTS test_auto_deleted")
|
||||
|
||||
|
||||
8
TEST.md
8
TEST.md
@ -18,10 +18,10 @@
|
||||
| `TestSchemaSync` | 通过 | 0.01s | 验证 DSL 同步、影子删除、版本号乐观锁及泛型 API |
|
||||
|
||||
## 🚀 性能基准 (Benchmarks)
|
||||
| 基准测试 | 迭代次数 | 耗时 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `BenchmarkForPool` | - | - | 已通过 (手动验证连接池复用) |
|
||||
| `BenchmarkForPoolParallel` | - | - | 已通过 (手动验证高并发下的稳定性) |
|
||||
| 基准测试 | 迭代次数 | 耗时 | 内存分配 | 备注 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `BenchmarkForPool` | 172009 | 7384 ns/op | 1224 B/op (34 allocs) | 验证 SQLite 下的查询绑定性能 |
|
||||
| `BenchmarkForPoolParallel` | 160250 | 6852 ns/op | 1296 B/op (35 allocs) | 验证高并发下的查询稳定性 |
|
||||
|
||||
## 🛠 环境
|
||||
- **OS**: darwin (macOS)
|
||||
|
||||
BIN
auto_detect.db
BIN
auto_detect.db
Binary file not shown.
@ -2,6 +2,7 @@ package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/db"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@ -26,14 +27,14 @@ func TestSmartDelete(t *testing.T) {
|
||||
|
||||
// Verify it's gone from main table
|
||||
qr := dbInst.Query("SELECT COUNT(*) FROM orders WHERE id = 1")
|
||||
count, _ := db.ToValue[int](qr)
|
||||
count, _ := db.To[int](qr)
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 records in main table, got %d", count)
|
||||
}
|
||||
|
||||
// Verify it's in shadow table
|
||||
qr2 := dbInst.Query("SELECT COUNT(*) FROM orders_deleted WHERE id = 1")
|
||||
countDeleted, _ := db.ToValue[int](qr2)
|
||||
countDeleted, _ := db.To[int](qr2)
|
||||
if countDeleted != 1 {
|
||||
t.Errorf("Expected 1 record in shadow table, got %d", countDeleted)
|
||||
}
|
||||
@ -46,7 +47,7 @@ func TestSmartDelete(t *testing.T) {
|
||||
dbInst.Delete("logs", "id = 1")
|
||||
|
||||
qr := dbInst.Query("SELECT COUNT(*) FROM logs WHERE id = 1")
|
||||
count, _ := db.ToValue[int](qr)
|
||||
count, _ := db.To[int](qr)
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 records in logs, got %d", count)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/db"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@ -36,7 +37,7 @@ func TestGenericQuery(t *testing.T) {
|
||||
|
||||
t.Run("ToValue", func(t *testing.T) {
|
||||
res := dbInst.Query("SELECT name FROM test_generic WHERE id = ?", 1)
|
||||
name, err := db.ToValue[string](res)
|
||||
name, err := db.To[string](res)
|
||||
if err != nil {
|
||||
t.Fatalf("ToValue failed: %v", err)
|
||||
}
|
||||
|
||||
BIN
test_schema.db
BIN
test_schema.db
Binary file not shown.
@ -1,7 +1,9 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/db"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@ -22,7 +24,7 @@ func TestVersionControl(t *testing.T) {
|
||||
// Verify version was injected
|
||||
var ver int64
|
||||
qr := dbInst.Query("SELECT autoVersion FROM users WHERE id = 1")
|
||||
ver, _ = db.ToValue[int64](qr)
|
||||
ver, _ = db.To[int64](qr)
|
||||
if ver != 1 {
|
||||
t.Errorf("Expected version 1, got %d", ver)
|
||||
}
|
||||
@ -42,7 +44,7 @@ func TestVersionControl(t *testing.T) {
|
||||
// Verify version incremented
|
||||
var ver int64
|
||||
qr := dbInst.Query("SELECT autoVersion FROM users WHERE id = 1")
|
||||
ver, _ = db.ToValue[int64](qr)
|
||||
ver, _ = db.To[int64](qr)
|
||||
if ver != 2 {
|
||||
t.Errorf("Expected version 2, got %d", ver)
|
||||
}
|
||||
@ -55,3 +57,34 @@ func TestVersionControl(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionInitialization(t *testing.T) {
|
||||
dbPath := "init_test.db"
|
||||
dbset := "sqlite://" + dbPath
|
||||
defer os.Remove(dbPath)
|
||||
|
||||
dbInst := db.GetDB(dbset, nil)
|
||||
dbInst.Exec("CREATE TABLE test_init (id INTEGER PRIMARY KEY, autoVersion BIGINT UNSIGNED)")
|
||||
|
||||
// Manually insert with a high version
|
||||
dbInst.Exec("INSERT INTO test_init (id, autoVersion) VALUES (1, 100)")
|
||||
|
||||
// First insert via DB helper should pick up 101
|
||||
data := map[string]any{"id": 2}
|
||||
res := dbInst.Insert("test_init", data)
|
||||
if res.Error != nil {
|
||||
t.Fatalf("Insert failed: %v", res.Error)
|
||||
}
|
||||
|
||||
ver, _ := db.To[int64](dbInst.Query("SELECT autoVersion FROM test_init WHERE id=2"))
|
||||
if ver != 101 {
|
||||
t.Errorf("Expected version 101, got %d", ver)
|
||||
}
|
||||
|
||||
// Update should make it 102
|
||||
dbInst.Update("test_init", map[string]any{"autoVersion": 101}, "id=2")
|
||||
ver, _ = db.To[int64](dbInst.Query("SELECT autoVersion FROM test_init WHERE id=2"))
|
||||
if ver != 102 {
|
||||
t.Errorf("Expected version 102, got %d", ver)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user