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:
hoteas 2026-01-22 07:16:42 +08:00
parent 0e1775f72b
commit c2955d2500
7 changed files with 756 additions and 447 deletions

File diff suppressed because one or more lines are too long

View 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
View File

@ -1,5 +1,5 @@
/.idea/* /.idea/*
.idea .idea
/example/tpt/demo/ /example/tpt/demo/
/*.exe *.exe
/example/config /example/config

View File

@ -223,15 +223,16 @@ func (that *HoTimeDB) buildJoin(joinData interface{}) string {
// Get 获取单条记录 // Get 获取单条记录
func (that *HoTimeDB) Get(table string, qu ...interface{}) Map { 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}) qu = append(qu, Map{"LIMIT": 1})
} } else if len(qu) == 2 {
if len(qu) == 2 {
temp := qu[1].(Map) temp := qu[1].(Map)
temp["LIMIT"] = 1 temp["LIMIT"] = 1
qu[1] = temp qu[1] = temp
} } else if len(qu) == 3 {
if len(qu) == 3 {
temp := qu[2].(Map) temp := qu[2].(Map)
temp["LIMIT"] = 1 temp["LIMIT"] = 1
qu[2] = temp qu[2] = temp

View File

@ -1,12 +1,13 @@
package db package db
import ( import (
. "code.hoteas.com/golang/hotime/common"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
. "code.hoteas.com/golang/hotime/common"
) )
// md5 生成查询的 MD5 哈希(用于缓存) // md5 生成查询的 MD5 哈希(用于缓存)
@ -23,6 +24,9 @@ func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
// queryWithRetry 内部查询方法,支持重试标记 // queryWithRetry 内部查询方法,支持重试标记
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map { func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护) // 保存调试信息(加锁保护)
that.mu.Lock() that.mu.Lock()
that.LastQuery = query that.LastQuery = query
@ -82,6 +86,9 @@ func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Erro
// execWithRetry 内部执行方法,支持重试标记 // execWithRetry 内部执行方法,支持重试标记
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) { func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护) // 保存调试信息(加锁保护)
that.mu.Lock() that.mu.Lock()
that.LastQuery = query that.LastQuery = query
@ -155,6 +162,202 @@ func (that *HoTimeDB) processArgs(args []interface{}) []interface{} {
return processedArgs 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 数据库数据解析 // Row 数据库数据解析
func (that *HoTimeDB) Row(resl *sql.Rows) []Map { func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
dest := make([]Map, 0) dest := make([]Map, 0)

View File

@ -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 { 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 continue
} }
if v != nil && strings.Contains(reflect.ValueOf(v).Type().String(), "[]") && len(ObjToSlice(v)) == 0 { 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 continue
} }
@ -110,17 +129,22 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
} }
// 添加 WHERE 关键字 // 添加 WHERE 关键字
if len(where) != 0 { // 先去除首尾空格,检查是否有实际条件内容
trimmedWhere := strings.TrimSpace(where)
if len(trimmedWhere) != 0 {
hasWhere := true hasWhere := true
for _, v := range vcond { for _, v := range vcond {
if strings.Index(where, v) == 0 { if strings.Index(trimmedWhere, v) == 0 {
hasWhere = false hasWhere = false
} }
} }
if hasWhere { if hasWhere {
where = " WHERE " + where + " " where = " WHERE " + trimmedWhere + " "
} }
} else {
// 没有实际条件内容,重置 where
where = ""
} }
// 处理特殊字符按固定顺序GROUP, HAVING, ORDER, LIMIT, OFFSET // 处理特殊字符按固定顺序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(), "[]") { if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v) vs := ObjToSlice(v)
if len(vs) == 0 { if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res 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(), "[]") { } else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v) vs := ObjToSlice(v)
if len(vs) == 0 { if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res return where, res
} }

View File

@ -1,9 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"time" "time"
. "code.hoteas.com/golang/hotime" . "code.hoteas.com/golang/hotime"
@ -11,33 +9,7 @@ import (
. "code.hoteas.com/golang/hotime/db" . "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() { func main() {
debugLog("main.go:35", "开始 HoTimeDB 全功能测试", nil, "START")
appIns := Init("config/config.json") appIns := Init("config/config.json")
appIns.SetConnectListener(func(that *Context) (isFinished bool) { appIns.SetConnectListener(func(that *Context) (isFinished bool) {
return isFinished return isFinished
@ -49,7 +21,6 @@ func main() {
// 测试入口 - 运行所有测试 // 测试入口 - 运行所有测试
"all": func(that *Context) { "all": func(that *Context) {
results := Map{} results := Map{}
debugLog("main.go:48", "开始所有测试", nil, "ALL_TESTS")
// 初始化测试表 // 初始化测试表
initTestTables(that) initTestTables(that)
@ -84,16 +55,13 @@ func main() {
// 10. 原生 SQL 测试 // 10. 原生 SQL 测试
results["10_raw_sql"] = testRawSQL(that) results["10_raw_sql"] = testRawSQL(that)
debugLog("main.go:80", "所有测试完成", results, "ALL_TESTS_DONE")
that.Display(0, results) that.Display(0, results)
}, },
// 查询数据库表结构 // 查询数据库表结构
"tables": func(that *Context) { "tables": func(that *Context) {
debugLog("main.go:tables", "查询数据库表结构", nil, "TABLES")
// 查询所有表 // 查询所有表
tables := that.Db.Query("SHOW TABLES") tables := that.Db.Query("SHOW TABLES")
debugLog("main.go:tables", "表列表", tables, "TABLES")
that.Display(0, Map{"tables": tables}) that.Display(0, Map{"tables": tables})
}, },
@ -108,7 +76,6 @@ func main() {
columns := that.Db.Query("DESCRIBE " + tableName) columns := that.Db.Query("DESCRIBE " + tableName)
// 查询表数据前10条 // 查询表数据前10条
data := that.Db.Select(tableName, Map{"LIMIT": 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}) that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data})
}, },
@ -144,11 +111,6 @@ func initTestTables(that *Context) {
adminCount := that.Db.Count("admin") adminCount := that.Db.Count("admin")
articleCount := that.Db.Count("article") articleCount := that.Db.Count("article")
debugLog("main.go:init", "MySQL数据库初始化检查完成", Map{
"adminCount": adminCount,
"articleCount": articleCount,
"dbType": "MySQL",
}, "INIT")
} }
// ==================== 1. 基础 CRUD 测试 ==================== // ==================== 1. 基础 CRUD 测试 ====================
@ -156,8 +118,6 @@ func testBasicCRUD(that *Context) Map {
result := Map{"name": "基础CRUD测试", "tests": Slice{}} result := Map{"name": "基础CRUD测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:103", "开始基础 CRUD 测试 (MySQL)", nil, "H1_CRUD")
// 1.1 Insert 测试 - 使用 admin 表 // 1.1 Insert 测试 - 使用 admin 表
insertTest := Map{"name": "Insert 插入测试 (admin表)"} insertTest := Map{"name": "Insert 插入测试 (admin表)"}
adminId := that.Db.Insert("admin", Map{ adminId := that.Db.Insert("admin", Map{
@ -173,7 +133,6 @@ func testBasicCRUD(that *Context) Map {
insertTest["result"] = adminId > 0 insertTest["result"] = adminId > 0
insertTest["adminId"] = adminId insertTest["adminId"] = adminId
insertTest["lastQuery"] = that.Db.LastQuery 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) tests = append(tests, insertTest)
// 1.2 Get 测试 // 1.2 Get 测试
@ -181,7 +140,6 @@ func testBasicCRUD(that *Context) Map {
admin := that.Db.Get("admin", "*", Map{"id": adminId}) admin := that.Db.Get("admin", "*", Map{"id": adminId})
getTest["result"] = admin != nil && admin.GetInt64("id") == adminId getTest["result"] = admin != nil && admin.GetInt64("id") == adminId
getTest["admin"] = admin getTest["admin"] = admin
debugLog("main.go:126", "Get 测试", Map{"admin": admin, "success": admin != nil}, "H1_GET")
tests = append(tests, getTest) tests = append(tests, getTest)
// 1.3 Select 测试 - 单条件 // 1.3 Select 测试 - 单条件
@ -189,7 +147,6 @@ func testBasicCRUD(that *Context) Map {
admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5}) admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5})
selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据 selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据
selectTest1["count"] = len(admins1) selectTest1["count"] = len(admins1)
debugLog("main.go:134", "Select 单条件测试", Map{"count": len(admins1)}, "H1_SELECT1")
tests = append(tests, selectTest1) tests = append(tests, selectTest1)
// 1.4 Select 测试 - 多条件(自动 AND // 1.4 Select 测试 - 多条件(自动 AND
@ -203,7 +160,6 @@ func testBasicCRUD(that *Context) Map {
selectTest2["result"] = true // 只要不报错就算成功 selectTest2["result"] = true // 只要不报错就算成功
selectTest2["count"] = len(admins2) selectTest2["count"] = len(admins2)
selectTest2["lastQuery"] = that.Db.LastQuery 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) tests = append(tests, selectTest2)
// 1.5 Update 测试 // 1.5 Update 测试
@ -214,7 +170,6 @@ func testBasicCRUD(that *Context) Map {
}, Map{"id": adminId}) }, Map{"id": adminId})
updateTest["result"] = affected > 0 updateTest["result"] = affected > 0
updateTest["affected"] = affected updateTest["affected"] = affected
debugLog("main.go:160", "Update 测试", Map{"affected": affected}, "H1_UPDATE")
tests = append(tests, updateTest) tests = append(tests, updateTest)
// 1.6 Delete 测试 - 使用 test_batch 表 // 1.6 Delete 测试 - 使用 test_batch 表
@ -956,6 +911,131 @@ func testRawSQL(that *Context) Map {
debugLog("main.go:899", "Exec 原生执行测试", test2, "H10_EXEC") debugLog("main.go:899", "Exec 原生执行测试", test2, "H10_EXEC")
tests = append(tests, test2) 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["tests"] = tests
result["success"] = true result["success"] = true
return result return result