feat(db): 实现数据库查询中的数组参数展开和空数组处理
- 在 Get 方法中添加无参数时的默认字段和 LIMIT 1 处理 - 实现 expandArrayPlaceholder 方法,自动展开 IN (?) 和 NOT IN (?) 中的数组参数 - 为空数组的 IN 条件生成 1=0 永假条件,NOT IN 生成 1=1 永真条件 - 在 queryWithRetry 和 execWithRetry 中集成数组占位符预处理 - 修复 where.go 中空切片条件的处理逻辑 - 添加完整的 IN/NOT IN 数组查询测试用例 - 更新 .gitignore 规则格式
This commit is contained in:
parent
0e1775f72b
commit
c2955d2500
File diff suppressed because one or more lines are too long
208
.cursor/plans/多数据库方言与前缀支持_979672ee.plan.md
Normal file
208
.cursor/plans/多数据库方言与前缀支持_979672ee.plan.md
Normal file
@ -0,0 +1,208 @@
|
||||
---
|
||||
name: 多数据库方言与前缀支持
|
||||
overview: 为 HoTimeDB ORM 实现完整的多数据库(MySQL/PostgreSQL/SQLite)方言支持和自动表前缀功能,同时保持完全向后兼容。
|
||||
todos:
|
||||
- id: dialect-interface
|
||||
content: 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
|
||||
status: pending
|
||||
- id: identifier-processor
|
||||
content: 实现 IdentifierProcessor 结构体及其方法
|
||||
status: pending
|
||||
- id: db-integration
|
||||
content: 在 HoTimeDB 中集成处理器,添加辅助方法
|
||||
status: pending
|
||||
- id: crud-update
|
||||
content: 修改 crud.go 中的所有 CRUD 方法使用新处理器
|
||||
status: pending
|
||||
- id: where-update
|
||||
content: 修改 where.go 中的条件处理逻辑
|
||||
status: pending
|
||||
- id: builder-update
|
||||
content: 修改 builder.go 中的链式 JOIN 方法
|
||||
status: pending
|
||||
- id: testing
|
||||
content: 测试多数据库和前缀功能
|
||||
status: pending
|
||||
---
|
||||
|
||||
# HoTimeDB 多数据库方言与自动前缀支持计划
|
||||
|
||||
## 目标
|
||||
|
||||
1. **多数据库方言支持**:让所有 ORM 方法正确支持 MySQL、PostgreSQL、SQLite 的标识符引号格式
|
||||
2. **自动表前缀**:在主表、JOIN 表、WHERE/ON 条件中自动识别并添加表前缀
|
||||
3. **完全向后兼容**:用户现有写法(`order.name`、`` `order`.name ``)无需修改
|
||||
|
||||
## 核心设计
|
||||
|
||||
### 标识符处理流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Input["用户输入: order.name 或 `order`.name"]
|
||||
Parse["解析标识符"]
|
||||
AddPrefix["添加表前缀"]
|
||||
QuoteByDialect["根据数据库类型添加引号"]
|
||||
Output["输出: `app_order`.`name` (MySQL) 或 \"app_order\".\"name\" (PG)"]
|
||||
|
||||
Input --> Parse
|
||||
Parse --> AddPrefix
|
||||
AddPrefix --> QuoteByDialect
|
||||
QuoteByDialect --> Output
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### 第1步:扩展 Dialect 接口([db/dialect.go](db/dialect.go))
|
||||
|
||||
在现有 `Dialect` 接口中添加新方法:
|
||||
|
||||
```go
|
||||
// QuoteIdentifier 处理标识符(支持 table.column 格式)
|
||||
// 输入: "order" 或 "order.name" 或 "`order`.name"
|
||||
// 输出: 带正确引号的标识符
|
||||
QuoteIdentifier(name string) string
|
||||
|
||||
// QuoteChar 获取引号字符(用于字符串中的替换检测)
|
||||
QuoteChar() string
|
||||
```
|
||||
|
||||
为三种数据库实现这些方法,核心逻辑:
|
||||
|
||||
- 去除已有的引号(支持反引号和双引号)
|
||||
- 按点号分割,对每部分单独加引号
|
||||
- MySQL 使用反引号,PostgreSQL/SQLite 使用双引号
|
||||
|
||||
### 第2步:添加标识符处理器([db/dialect.go](db/dialect.go))
|
||||
|
||||
新增 `IdentifierProcessor` 结构体:
|
||||
|
||||
```go
|
||||
type IdentifierProcessor struct {
|
||||
dialect Dialect
|
||||
prefix string
|
||||
}
|
||||
|
||||
// ProcessTableName 处理表名(添加前缀+引号)
|
||||
func (p *IdentifierProcessor) ProcessTableName(name string) string
|
||||
|
||||
// ProcessColumn 处理字段名(table.column 格式,自动给表名加前缀)
|
||||
func (p *IdentifierProcessor) ProcessColumn(name string) string
|
||||
|
||||
// ProcessCondition 处理 ON/WHERE 条件字符串中的 table.column
|
||||
func (p *IdentifierProcessor) ProcessCondition(condition string) string
|
||||
```
|
||||
|
||||
关键实现细节:
|
||||
|
||||
- 使用正则表达式识别条件字符串中的 `table.column` 模式
|
||||
- 识别已有的引号包裹(`` `table`.column `` 或 `"table".column`)
|
||||
- 避免误处理字符串字面量中的内容
|
||||
|
||||
### 第3步:在 HoTimeDB 中集成处理器([db/db.go](db/db.go))
|
||||
|
||||
```go
|
||||
// GetProcessor 获取标识符处理器(懒加载)
|
||||
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor
|
||||
|
||||
// processTable 内部方法:处理表名
|
||||
func (that *HoTimeDB) processTable(table string) string
|
||||
|
||||
// processColumn 内部方法:处理字段名
|
||||
func (that *HoTimeDB) processColumn(column string) string
|
||||
```
|
||||
|
||||
### 第4步:修改 CRUD 方法([db/crud.go](db/crud.go))
|
||||
|
||||
需要修改的方法及位置:
|
||||
|
||||
| 方法 | 修改内容 |
|
||||
|
||||
|------|---------|
|
||||
|
||||
| `Select` (L77-153) | 表名、字段名处理 |
|
||||
|
||||
| `buildJoin` (L156-222) | JOIN 表名、ON 条件处理 |
|
||||
|
||||
| `Insert` (L248-302) | 表名、字段名处理 |
|
||||
|
||||
| `BatchInsert` (L316-388) | 表名、字段名处理 |
|
||||
|
||||
| `Update` (L598-638) | 表名、字段名处理 |
|
||||
|
||||
| `Delete` (L640-661) | 表名处理 |
|
||||
|
||||
核心改动模式(以 Select 为例):
|
||||
|
||||
```go
|
||||
// 之前
|
||||
query += " FROM `" + that.Prefix + table + "` "
|
||||
// 之后
|
||||
query += " FROM " + that.processTable(table) + " "
|
||||
```
|
||||
|
||||
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
||||
|
||||
修改 `varCond` 方法(L205-338)中的字段名处理:
|
||||
|
||||
```go
|
||||
// 之前
|
||||
if !strings.Contains(k, ".") {
|
||||
k = "`" + k + "`"
|
||||
}
|
||||
// 之后
|
||||
k = that.processColumn(k)
|
||||
```
|
||||
|
||||
同样修改 `handlePlainField`、`handleDefaultCondition` 等方法。
|
||||
|
||||
### 第6步:修改链式构建器([db/builder.go](db/builder.go))
|
||||
|
||||
`LeftJoin`、`RightJoin` 等方法中的表名和条件字符串也需要处理:
|
||||
|
||||
```go
|
||||
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
||||
// 处理表名和 ON 条件
|
||||
table = that.HoTimeDB.processTable(table)
|
||||
joinStr = that.HoTimeDB.GetProcessor().ProcessCondition(joinStr)
|
||||
that.Join(Map{"[>]" + table: joinStr})
|
||||
return that
|
||||
}
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **避免误替换数据**:条件处理只处理 SQL 语法结构中的标识符,不处理:
|
||||
|
||||
- 字符串字面量内的内容(通过检测引号边界)
|
||||
- 已经是占位符 `?` 的参数值
|
||||
|
||||
2. **正则表达式设计**:识别 `table.column` 模式时排除:
|
||||
|
||||
- 数字开头的标识符
|
||||
- 函数调用如 `NOW()`
|
||||
- 特殊运算符如 `>=`、`<=`
|
||||
|
||||
## 测试要点
|
||||
|
||||
1. 多数据库类型切换
|
||||
2. 带前缀和不带前缀场景
|
||||
3. 复杂 JOIN 查询
|
||||
4. 嵌套条件查询
|
||||
5. 特殊字符和保留字作为表名/字段名
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
| 文件 | 修改类型 |
|
||||
|
||||
|------|---------|
|
||||
|
||||
| [db/dialect.go](db/dialect.go) | 扩展接口,添加 IdentifierProcessor |
|
||||
|
||||
| [db/db.go](db/db.go) | 添加 GetProcessor 和辅助方法 |
|
||||
|
||||
| [db/crud.go](db/crud.go) | 修改所有 CRUD 方法 |
|
||||
|
||||
| [db/where.go](db/where.go) | 修改条件处理逻辑 |
|
||||
|
||||
| [db/builder.go](db/builder.go) | 修改链式构建器的 JOIN 方法 |
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
/.idea/*
|
||||
.idea
|
||||
/example/tpt/demo/
|
||||
/*.exe
|
||||
*.exe
|
||||
/example/config
|
||||
11
db/crud.go
11
db/crud.go
@ -223,15 +223,16 @@ func (that *HoTimeDB) buildJoin(joinData interface{}) string {
|
||||
|
||||
// Get 获取单条记录
|
||||
func (that *HoTimeDB) Get(table string, qu ...interface{}) Map {
|
||||
if len(qu) == 1 {
|
||||
if len(qu) == 0 {
|
||||
// 没有参数时,添加默认字段和 LIMIT
|
||||
qu = append(qu, "*", Map{"LIMIT": 1})
|
||||
} else if len(qu) == 1 {
|
||||
qu = append(qu, Map{"LIMIT": 1})
|
||||
}
|
||||
if len(qu) == 2 {
|
||||
} else if len(qu) == 2 {
|
||||
temp := qu[1].(Map)
|
||||
temp["LIMIT"] = 1
|
||||
qu[1] = temp
|
||||
}
|
||||
if len(qu) == 3 {
|
||||
} else if len(qu) == 3 {
|
||||
temp := qu[2].(Map)
|
||||
temp["LIMIT"] = 1
|
||||
qu[2] = temp
|
||||
|
||||
205
db/query.go
205
db/query.go
@ -1,12 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
)
|
||||
|
||||
// md5 生成查询的 MD5 哈希(用于缓存)
|
||||
@ -23,6 +24,9 @@ func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
|
||||
|
||||
// queryWithRetry 内部查询方法,支持重试标记
|
||||
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
|
||||
// 预处理数组占位符 ?[]
|
||||
query, args = that.expandArrayPlaceholder(query, args)
|
||||
|
||||
// 保存调试信息(加锁保护)
|
||||
that.mu.Lock()
|
||||
that.LastQuery = query
|
||||
@ -82,6 +86,9 @@ func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Erro
|
||||
|
||||
// execWithRetry 内部执行方法,支持重试标记
|
||||
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
|
||||
// 预处理数组占位符 ?[]
|
||||
query, args = that.expandArrayPlaceholder(query, args)
|
||||
|
||||
// 保存调试信息(加锁保护)
|
||||
that.mu.Lock()
|
||||
that.LastQuery = query
|
||||
@ -155,6 +162,202 @@ func (that *HoTimeDB) processArgs(args []interface{}) []interface{} {
|
||||
return processedArgs
|
||||
}
|
||||
|
||||
// expandArrayPlaceholder 展开 IN (?) / NOT IN (?) 中的数组参数
|
||||
// 自动识别 IN/NOT IN (?) 模式,当参数是数组时展开为多个 ?
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{1, 2, 3})
|
||||
// // 展开为: SELECT * FROM user WHERE id IN (?, ?, ?) 参数: [1, 2, 3]
|
||||
//
|
||||
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{})
|
||||
// // 展开为: SELECT * FROM user WHERE 1=0 参数: [] (空集合的IN永假)
|
||||
//
|
||||
// db.Query("SELECT * FROM user WHERE id NOT IN (?)", []int{})
|
||||
// // 展开为: SELECT * FROM user WHERE 1=1 参数: [] (空集合的NOT IN永真)
|
||||
//
|
||||
// db.Query("SELECT * FROM user WHERE id = ?", 1)
|
||||
// // 保持不变: SELECT * FROM user WHERE id = ? 参数: [1]
|
||||
func (that *HoTimeDB) expandArrayPlaceholder(query string, args []interface{}) (string, []interface{}) {
|
||||
if len(args) == 0 || !strings.Contains(query, "?") {
|
||||
return query, args
|
||||
}
|
||||
|
||||
// 检查是否有数组参数
|
||||
hasArray := false
|
||||
for _, arg := range args {
|
||||
if arg == nil {
|
||||
continue
|
||||
}
|
||||
argType := reflect.ValueOf(arg).Type().String()
|
||||
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
|
||||
hasArray = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasArray {
|
||||
return query, args
|
||||
}
|
||||
|
||||
newArgs := make([]interface{}, 0, len(args))
|
||||
result := strings.Builder{}
|
||||
argIndex := 0
|
||||
|
||||
for i := 0; i < len(query); i++ {
|
||||
if query[i] == '?' && argIndex < len(args) {
|
||||
arg := args[argIndex]
|
||||
argIndex++
|
||||
|
||||
if arg == nil {
|
||||
result.WriteByte('?')
|
||||
newArgs = append(newArgs, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
argType := reflect.ValueOf(arg).Type().String()
|
||||
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
|
||||
// 是数组参数,检查是否在 IN (...) 或 NOT IN (...) 中
|
||||
prevPart := result.String()
|
||||
prevUpper := strings.ToUpper(prevPart)
|
||||
|
||||
// 查找最近的 NOT IN ( 模式
|
||||
notInIndex := strings.LastIndex(prevUpper, " NOT IN (")
|
||||
notInIndex2 := strings.LastIndex(prevUpper, " NOT IN(")
|
||||
if notInIndex2 > notInIndex {
|
||||
notInIndex = notInIndex2
|
||||
}
|
||||
|
||||
// 查找最近的 IN ( 模式(但要排除 NOT IN 的情况)
|
||||
inIndex := strings.LastIndex(prevUpper, " IN (")
|
||||
inIndex2 := strings.LastIndex(prevUpper, " IN(")
|
||||
if inIndex2 > inIndex {
|
||||
inIndex = inIndex2
|
||||
}
|
||||
|
||||
// 判断是 NOT IN 还是 IN
|
||||
// 注意:" NOT IN (" 包含 " IN (",所以如果找到的 IN 位置在 NOT IN 范围内,应该优先判断为 NOT IN
|
||||
isNotIn := false
|
||||
matchIndex := -1
|
||||
if notInIndex != -1 {
|
||||
// 检查 inIndex 是否在 notInIndex 范围内(即 NOT IN 的 IN 部分)
|
||||
// NOT IN ( 的 IN ( 部分从 notInIndex + 4 开始
|
||||
if inIndex != -1 && inIndex >= notInIndex && inIndex <= notInIndex+5 {
|
||||
// inIndex 是 NOT IN 的一部分,使用 NOT IN
|
||||
isNotIn = true
|
||||
matchIndex = notInIndex
|
||||
} else if inIndex == -1 || notInIndex > inIndex {
|
||||
// 没有独立的 IN,或 NOT IN 在 IN 之后
|
||||
isNotIn = true
|
||||
matchIndex = notInIndex
|
||||
} else {
|
||||
// 有独立的 IN 且在 NOT IN 之后
|
||||
matchIndex = inIndex
|
||||
}
|
||||
} else if inIndex != -1 {
|
||||
matchIndex = inIndex
|
||||
}
|
||||
|
||||
// 检查 IN ( 后面是否只有空格(即当前 ? 紧跟在 IN ( 后面)
|
||||
isInPattern := false
|
||||
if matchIndex != -1 {
|
||||
afterIn := prevPart[matchIndex:]
|
||||
// 找到 ( 的位置
|
||||
parenIdx := strings.Index(afterIn, "(")
|
||||
if parenIdx != -1 {
|
||||
afterParen := strings.TrimSpace(afterIn[parenIdx+1:])
|
||||
if afterParen == "" {
|
||||
isInPattern = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isInPattern {
|
||||
// 在 IN (...) 或 NOT IN (...) 模式中
|
||||
argList := ObjToSlice(arg)
|
||||
if len(argList) == 0 {
|
||||
// 空数组处理:需要找到字段名的开始位置
|
||||
// 往前找最近的 AND/OR/WHERE/(,以确定条件的开始位置
|
||||
truncateIndex := matchIndex
|
||||
searchPart := prevUpper[:matchIndex]
|
||||
|
||||
// 找最近的分隔符位置
|
||||
andIdx := strings.LastIndex(searchPart, " AND ")
|
||||
orIdx := strings.LastIndex(searchPart, " OR ")
|
||||
whereIdx := strings.LastIndex(searchPart, " WHERE ")
|
||||
parenIdx := strings.LastIndex(searchPart, "(")
|
||||
|
||||
// 取最靠后的分隔符
|
||||
sepIndex := -1
|
||||
sepLen := 0
|
||||
if andIdx > sepIndex {
|
||||
sepIndex = andIdx
|
||||
sepLen = 5 // " AND "
|
||||
}
|
||||
if orIdx > sepIndex {
|
||||
sepIndex = orIdx
|
||||
sepLen = 4 // " OR "
|
||||
}
|
||||
if whereIdx > sepIndex {
|
||||
sepIndex = whereIdx
|
||||
sepLen = 7 // " WHERE "
|
||||
}
|
||||
if parenIdx > sepIndex {
|
||||
sepIndex = parenIdx
|
||||
sepLen = 1 // "("
|
||||
}
|
||||
|
||||
if sepIndex != -1 {
|
||||
truncateIndex = sepIndex + sepLen
|
||||
}
|
||||
|
||||
result.Reset()
|
||||
result.WriteString(prevPart[:truncateIndex])
|
||||
if isNotIn {
|
||||
// NOT IN 空集合 = 永真
|
||||
result.WriteString(" 1=1 ")
|
||||
} else {
|
||||
// IN 空集合 = 永假
|
||||
result.WriteString(" 1=0 ")
|
||||
}
|
||||
// 跳过后面的 )
|
||||
for j := i + 1; j < len(query); j++ {
|
||||
if query[j] == ')' {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if len(argList) == 1 {
|
||||
// 单元素数组
|
||||
result.WriteByte('?')
|
||||
newArgs = append(newArgs, argList[0])
|
||||
} else {
|
||||
// 多元素数组,展开为多个 ?
|
||||
for j := 0; j < len(argList); j++ {
|
||||
if j > 0 {
|
||||
result.WriteString(", ")
|
||||
}
|
||||
result.WriteByte('?')
|
||||
newArgs = append(newArgs, argList[j])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不在 IN 模式中,保持原有行为(数组会被 processArgs 转为逗号字符串)
|
||||
result.WriteByte('?')
|
||||
newArgs = append(newArgs, arg)
|
||||
}
|
||||
} else {
|
||||
// 非数组参数
|
||||
result.WriteByte('?')
|
||||
newArgs = append(newArgs, arg)
|
||||
}
|
||||
} else {
|
||||
result.WriteByte(query[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), newArgs
|
||||
}
|
||||
|
||||
// Row 数据库数据解析
|
||||
func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
|
||||
dest := make([]Map, 0)
|
||||
|
||||
34
db/where.go
34
db/where.go
@ -90,10 +90,29 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
||||
}
|
||||
|
||||
// 处理普通条件字段
|
||||
// 空切片的 IN 条件应该生成永假条件(1=0),而不是跳过
|
||||
if v != nil && reflect.ValueOf(v).Type().String() == "common.Slice" && len(v.(Slice)) == 0 {
|
||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
||||
if !strings.HasSuffix(k, "[!]") {
|
||||
// IN 空数组 -> 生成永假条件
|
||||
if normalCondCount > 0 {
|
||||
where += " AND "
|
||||
}
|
||||
where += "1=0 "
|
||||
normalCondCount++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if v != nil && strings.Contains(reflect.ValueOf(v).Type().String(), "[]") && len(ObjToSlice(v)) == 0 {
|
||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
||||
if !strings.HasSuffix(k, "[!]") {
|
||||
// IN 空数组 -> 生成永假条件
|
||||
if normalCondCount > 0 {
|
||||
where += " AND "
|
||||
}
|
||||
where += "1=0 "
|
||||
normalCondCount++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -110,17 +129,22 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
||||
}
|
||||
|
||||
// 添加 WHERE 关键字
|
||||
if len(where) != 0 {
|
||||
// 先去除首尾空格,检查是否有实际条件内容
|
||||
trimmedWhere := strings.TrimSpace(where)
|
||||
if len(trimmedWhere) != 0 {
|
||||
hasWhere := true
|
||||
for _, v := range vcond {
|
||||
if strings.Index(where, v) == 0 {
|
||||
if strings.Index(trimmedWhere, v) == 0 {
|
||||
hasWhere = false
|
||||
}
|
||||
}
|
||||
|
||||
if hasWhere {
|
||||
where = " WHERE " + where + " "
|
||||
where = " WHERE " + trimmedWhere + " "
|
||||
}
|
||||
} else {
|
||||
// 没有实际条件内容,重置 where
|
||||
where = ""
|
||||
}
|
||||
|
||||
// 处理特殊字符(按固定顺序:GROUP, HAVING, ORDER, LIMIT, OFFSET)
|
||||
@ -322,6 +346,8 @@ func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where stri
|
||||
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
||||
vs := ObjToSlice(v)
|
||||
if len(vs) == 0 {
|
||||
// IN 空数组 -> 生成永假条件
|
||||
where += "1=0 "
|
||||
return where, res
|
||||
}
|
||||
|
||||
@ -352,6 +378,8 @@ func (that *HoTimeDB) handlePlainField(k string, v interface{}, where string, re
|
||||
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
||||
vs := ObjToSlice(v)
|
||||
if len(vs) == 0 {
|
||||
// IN 空数组 -> 生成永假条件
|
||||
where += "1=0 "
|
||||
return where, res
|
||||
}
|
||||
|
||||
|
||||
170
example/main.go
170
example/main.go
@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
. "code.hoteas.com/golang/hotime"
|
||||
@ -11,33 +9,7 @@ import (
|
||||
. "code.hoteas.com/golang/hotime/db"
|
||||
)
|
||||
|
||||
// 调试日志文件路径
|
||||
const debugLogPath = `d:\work\hotimev1\.cursor\debug.log`
|
||||
|
||||
// debugLog 写入调试日志
|
||||
func debugLog(location, message string, data interface{}, hypothesisId string) {
|
||||
// #region agent log
|
||||
logEntry := Map{
|
||||
"location": location,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
"sessionId": "debug-session",
|
||||
"runId": "hotimedb-test-run",
|
||||
"hypothesisId": hypothesisId,
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(logEntry)
|
||||
f, err := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err == nil {
|
||||
f.WriteString(string(jsonBytes) + "\n")
|
||||
f.Close()
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
func main() {
|
||||
debugLog("main.go:35", "开始 HoTimeDB 全功能测试", nil, "START")
|
||||
|
||||
appIns := Init("config/config.json")
|
||||
appIns.SetConnectListener(func(that *Context) (isFinished bool) {
|
||||
return isFinished
|
||||
@ -49,7 +21,6 @@ func main() {
|
||||
// 测试入口 - 运行所有测试
|
||||
"all": func(that *Context) {
|
||||
results := Map{}
|
||||
debugLog("main.go:48", "开始所有测试", nil, "ALL_TESTS")
|
||||
|
||||
// 初始化测试表
|
||||
initTestTables(that)
|
||||
@ -84,16 +55,13 @@ func main() {
|
||||
// 10. 原生 SQL 测试
|
||||
results["10_raw_sql"] = testRawSQL(that)
|
||||
|
||||
debugLog("main.go:80", "所有测试完成", results, "ALL_TESTS_DONE")
|
||||
that.Display(0, results)
|
||||
},
|
||||
|
||||
// 查询数据库表结构
|
||||
"tables": func(that *Context) {
|
||||
debugLog("main.go:tables", "查询数据库表结构", nil, "TABLES")
|
||||
// 查询所有表
|
||||
tables := that.Db.Query("SHOW TABLES")
|
||||
debugLog("main.go:tables", "表列表", tables, "TABLES")
|
||||
that.Display(0, Map{"tables": tables})
|
||||
},
|
||||
|
||||
@ -108,7 +76,6 @@ func main() {
|
||||
columns := that.Db.Query("DESCRIBE " + tableName)
|
||||
// 查询表数据(前10条)
|
||||
data := that.Db.Select(tableName, Map{"LIMIT": 10})
|
||||
debugLog("main.go:describe", "表结构", Map{"table": tableName, "columns": columns, "data": data}, "DESCRIBE")
|
||||
that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data})
|
||||
},
|
||||
|
||||
@ -144,11 +111,6 @@ func initTestTables(that *Context) {
|
||||
adminCount := that.Db.Count("admin")
|
||||
articleCount := that.Db.Count("article")
|
||||
|
||||
debugLog("main.go:init", "MySQL数据库初始化检查完成", Map{
|
||||
"adminCount": adminCount,
|
||||
"articleCount": articleCount,
|
||||
"dbType": "MySQL",
|
||||
}, "INIT")
|
||||
}
|
||||
|
||||
// ==================== 1. 基础 CRUD 测试 ====================
|
||||
@ -156,8 +118,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
result := Map{"name": "基础CRUD测试", "tests": Slice{}}
|
||||
tests := Slice{}
|
||||
|
||||
debugLog("main.go:103", "开始基础 CRUD 测试 (MySQL)", nil, "H1_CRUD")
|
||||
|
||||
// 1.1 Insert 测试 - 使用 admin 表
|
||||
insertTest := Map{"name": "Insert 插入测试 (admin表)"}
|
||||
adminId := that.Db.Insert("admin", Map{
|
||||
@ -173,7 +133,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
insertTest["result"] = adminId > 0
|
||||
insertTest["adminId"] = adminId
|
||||
insertTest["lastQuery"] = that.Db.LastQuery
|
||||
debugLog("main.go:118", "Insert 测试", Map{"adminId": adminId, "success": adminId > 0, "query": that.Db.LastQuery}, "H1_INSERT")
|
||||
tests = append(tests, insertTest)
|
||||
|
||||
// 1.2 Get 测试
|
||||
@ -181,7 +140,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
admin := that.Db.Get("admin", "*", Map{"id": adminId})
|
||||
getTest["result"] = admin != nil && admin.GetInt64("id") == adminId
|
||||
getTest["admin"] = admin
|
||||
debugLog("main.go:126", "Get 测试", Map{"admin": admin, "success": admin != nil}, "H1_GET")
|
||||
tests = append(tests, getTest)
|
||||
|
||||
// 1.3 Select 测试 - 单条件
|
||||
@ -189,7 +147,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5})
|
||||
selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据
|
||||
selectTest1["count"] = len(admins1)
|
||||
debugLog("main.go:134", "Select 单条件测试", Map{"count": len(admins1)}, "H1_SELECT1")
|
||||
tests = append(tests, selectTest1)
|
||||
|
||||
// 1.4 Select 测试 - 多条件(自动 AND)
|
||||
@ -203,7 +160,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
selectTest2["result"] = true // 只要不报错就算成功
|
||||
selectTest2["count"] = len(admins2)
|
||||
selectTest2["lastQuery"] = that.Db.LastQuery
|
||||
debugLog("main.go:149", "Select 多条件自动AND测试", Map{"count": len(admins2), "query": that.Db.LastQuery}, "H1_SELECT2")
|
||||
tests = append(tests, selectTest2)
|
||||
|
||||
// 1.5 Update 测试
|
||||
@ -214,7 +170,6 @@ func testBasicCRUD(that *Context) Map {
|
||||
}, Map{"id": adminId})
|
||||
updateTest["result"] = affected > 0
|
||||
updateTest["affected"] = affected
|
||||
debugLog("main.go:160", "Update 测试", Map{"affected": affected}, "H1_UPDATE")
|
||||
tests = append(tests, updateTest)
|
||||
|
||||
// 1.6 Delete 测试 - 使用 test_batch 表
|
||||
@ -956,6 +911,131 @@ func testRawSQL(that *Context) Map {
|
||||
debugLog("main.go:899", "Exec 原生执行测试", test2, "H10_EXEC")
|
||||
tests = append(tests, test2)
|
||||
|
||||
// ==================== IN/NOT IN 数组测试 ====================
|
||||
// H1: IN (?) 配合非空数组能正确展开
|
||||
test3 := Map{"name": "H1: IN (?) 非空数组展开"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test3", "H1测试开始: IN非空数组", Map{"ids": []int{1, 2, 3, 4, 5}}, "H1")
|
||||
// #endregion
|
||||
articles3 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{1, 2, 3, 4, 5})
|
||||
// #region agent log
|
||||
debugLog("main.go:test3", "H1测试结果: IN非空数组", Map{
|
||||
"count": len(articles3),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
"data": articles3,
|
||||
}, "H1")
|
||||
// #endregion
|
||||
test3["result"] = len(articles3) >= 0
|
||||
test3["count"] = len(articles3)
|
||||
test3["lastQuery"] = that.Db.LastQuery
|
||||
tests = append(tests, test3)
|
||||
|
||||
// H2: IN (?) 配合空数组替换为 1=0
|
||||
test4 := Map{"name": "H2: IN (?) 空数组替换为1=0"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test4", "H2测试开始: IN空数组", Map{"ids": []int{}}, "H2")
|
||||
// #endregion
|
||||
articles4 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{})
|
||||
// #region agent log
|
||||
debugLog("main.go:test4", "H2测试结果: IN空数组", Map{
|
||||
"count": len(articles4),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
"expected": "应包含1=0,返回0条记录",
|
||||
}, "H2")
|
||||
// #endregion
|
||||
test4["result"] = len(articles4) == 0 // 空数组的IN应该返回0条
|
||||
test4["count"] = len(articles4)
|
||||
test4["lastQuery"] = that.Db.LastQuery
|
||||
test4["expected"] = "count=0, SQL应包含1=0"
|
||||
tests = append(tests, test4)
|
||||
|
||||
// H3: NOT IN (?) 配合空数组替换为 1=1
|
||||
test5 := Map{"name": "H3: NOT IN (?) 空数组替换为1=1"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test5", "H3测试开始: NOT IN空数组", Map{"ids": []int{}}, "H3")
|
||||
// #endregion
|
||||
articles5 := that.Db.Query("SELECT id, title FROM `article` WHERE id NOT IN (?) LIMIT 10", []int{})
|
||||
// #region agent log
|
||||
debugLog("main.go:test5", "H3测试结果: NOT IN空数组", Map{
|
||||
"count": len(articles5),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
"expected": "应包含1=1,返回所有记录(限制10条)",
|
||||
}, "H3")
|
||||
// #endregion
|
||||
test5["result"] = len(articles5) > 0 // NOT IN空数组应该返回记录
|
||||
test5["count"] = len(articles5)
|
||||
test5["lastQuery"] = that.Db.LastQuery
|
||||
test5["expected"] = "count>0, SQL应包含1=1"
|
||||
tests = append(tests, test5)
|
||||
|
||||
// H4: NOT IN (?) 配合非空数组正常展开
|
||||
test6 := Map{"name": "H4: NOT IN (?) 非空数组展开"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test6", "H4测试开始: NOT IN非空数组", Map{"ids": []int{1, 2, 3}}, "H4")
|
||||
// #endregion
|
||||
articles6 := that.Db.Query("SELECT id, title FROM `article` WHERE id NOT IN (?) LIMIT 10", []int{1, 2, 3})
|
||||
// #region agent log
|
||||
debugLog("main.go:test6", "H4测试结果: NOT IN非空数组", Map{
|
||||
"count": len(articles6),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
}, "H4")
|
||||
// #endregion
|
||||
test6["result"] = len(articles6) >= 0
|
||||
test6["count"] = len(articles6)
|
||||
test6["lastQuery"] = that.Db.LastQuery
|
||||
tests = append(tests, test6)
|
||||
|
||||
// H5: 普通 ? 占位符保持原有行为
|
||||
test7 := Map{"name": "H5: 普通?占位符不受影响"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test7", "H5测试开始: 普通占位符", Map{"state": 0, "limit": 5}, "H5")
|
||||
// #endregion
|
||||
articles7 := that.Db.Query("SELECT id, title FROM `article` WHERE state = ? LIMIT ?", 0, 5)
|
||||
// #region agent log
|
||||
debugLog("main.go:test7", "H5测试结果: 普通占位符", Map{
|
||||
"count": len(articles7),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
}, "H5")
|
||||
// #endregion
|
||||
test7["result"] = len(articles7) >= 0
|
||||
test7["count"] = len(articles7)
|
||||
test7["lastQuery"] = that.Db.LastQuery
|
||||
tests = append(tests, test7)
|
||||
|
||||
// 额外测试: Select ORM方法的空数组处理
|
||||
test8 := Map{"name": "ORM Select: IN空数组"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test8", "ORM测试开始: Select IN空数组", nil, "H2_ORM")
|
||||
// #endregion
|
||||
articles8 := that.Db.Select("article", "id,title", Map{"id": []int{}, "LIMIT": 10})
|
||||
// #region agent log
|
||||
debugLog("main.go:test8", "ORM测试结果: Select IN空数组", Map{
|
||||
"count": len(articles8),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
}, "H2_ORM")
|
||||
// #endregion
|
||||
test8["result"] = len(articles8) == 0
|
||||
test8["count"] = len(articles8)
|
||||
test8["lastQuery"] = that.Db.LastQuery
|
||||
tests = append(tests, test8)
|
||||
|
||||
// 额外测试: Select ORM方法的NOT IN空数组处理
|
||||
test9 := Map{"name": "ORM Select: NOT IN空数组"}
|
||||
// #region agent log
|
||||
debugLog("main.go:test9", "ORM测试开始: Select NOT IN空数组", nil, "H3_ORM")
|
||||
// #endregion
|
||||
articles9 := that.Db.Select("article", "id,title", Map{"id[!]": []int{}, "LIMIT": 10})
|
||||
// #region agent log
|
||||
debugLog("main.go:test9", "ORM测试结果: Select NOT IN空数组", Map{
|
||||
"count": len(articles9),
|
||||
"lastQuery": that.Db.LastQuery,
|
||||
}, "H3_ORM")
|
||||
// #endregion
|
||||
test9["result"] = len(articles9) > 0 // NOT IN 空数组应返回记录
|
||||
test9["count"] = len(articles9)
|
||||
test9["lastQuery"] = that.Db.LastQuery
|
||||
tests = append(tests, test9)
|
||||
|
||||
result["tests"] = tests
|
||||
result["success"] = true
|
||||
return result
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user