Compare commits

8 Commits
Author SHA1 Message Date
what dcfd2f42da docs: 补充 README,说明当前分支的能力范围
配合 v1-legacy 维护分支的拆分,记录 master 这条线(Condition/ConditionExpr
表达式树、TokenValue、JSON 序列化、engine 规则引擎)目前有哪些能力,跟
v1-legacy 分支做区分;顺带记一下 engine.go 那个已知的 nil user 问题。
2026-08-21 09:03:29 +08:00
what d6c3547690 chore: 清理 go.mod,移除本地 replace,db/reflux/req 锁定真实版本
本地多仓库协同开发改用父目录的 go.work,不用再靠这个仓库自己的 replace
指向兄弟目录;db/reflux/req 锁定成各自 master 最新的真实 commit。
2026-08-21 08:58:29 +08:00
what a14d0f2c72 feat: Condition/ConditionExpr 支持 JSON 序列化/反序列化
给 Condition/ConditionExpr 加 MarshalJSON/UnmarshalJSON,中间过一层 DTO
(conditionDTO/exprDTO)避免 UnmarshalJSON 递归。FromMap(m map[string]any)
方便从已经解析好的 map(比如请求参数)直接构造 Condition,不用先序列化
成 JSON 字符串再解析一遍。
2026-08-21 08:57:07 +08:00
what 68cd65f70d test: 补充 Condition.ToSql 迁移到表达式树后的测试覆盖 2026-08-21 08:56:37 +08:00
what 5b39565a36 重构: Condition/Engine 迁移到 db-v2 的表达式树,替换掉旧的 db.Raw 拼字符串
ToSql 之前是手工拼接 SQL 字符串(string 类型的 conditions、手动加括号、
手动拼 AND/OR),改成用 exp.NewExpressionList 构造表达式树,跟 db-v2
其余部分统一走"先建表达式树、由方言渲染成 SQL"这条路,不再自己维护一份
字符串拼接逻辑。ConditionType/ConditionOperator 相应改成 exp.ExpressionListType/
exp.BooleanOperation 的类型别名。

TokenValue 从 condition_expr.go 里独立成 token_value.go,从
GetParam(k string) req.GlobalParams 改成 GetParam(k string) valuex.Accessor
(配合 reflux/valuex),跟其它 "-v2" 项目的属性访问方式统一。engine 包
配合适配(sqlite3 用 db/engine 的新连接管理,SelectDataset 替代
db.Connection),删掉了不再需要的 EngineParam(老 GlobalParams 包装,
新 TokenValue 已经不需要这层)。

已知问题:engine.go 在没有外部传入 GlobalParams 时用 nil user 构造兜底
值,跟 NewTokenValue 现在强制要求非 nil user 冲突,TestEngine 会 panic
——这个先不修,等后面调整 Engine.Execute 让调用方能传 user 时一起解决。
2026-08-21 08:55:21 +08:00
what 50e1b3630e [feat] git.fsdpf.net/go/req 依赖更新 2024-11-26 17:36:38 +08:00
what c3471706f4 [feat] git.fsdpf.net/go/req 依赖更新 2024-11-26 17:01:16 +08:00
what bb13458114 [feat] git.fsdpf.net/go/req 依赖更新 2024-11-26 16:48:57 +08:00
15 changed files with 734 additions and 292 deletions
+1
View File
@@ -21,3 +21,4 @@
# Go workspace file # Go workspace file
go.work go.work
.DS_Store
+44 -28
View File
@@ -1,38 +1,54 @@
# git.fsdpf.net/go/conditionv1-legacy # git.fsdpf.net/go/conditionmaster
老架构下的条件表达式 DSL:把一组条件(等值/比较/模糊匹配/IN 等)组织成可嵌套的 `AND`/`OR` 条件表达式 DSL:把一组条件(等值/比较/模糊匹配/IN 等)组织成可嵌套的 `AND`/`OR` 树,渲染成
树,`ToSql` 手工拼接成 SQL 字符串(`db.Raw(...)`),供权限过滤、规则引擎等场景复用同一套 `db-v2` 的表达式(`db.Expression`),供权限过滤、规则引擎等场景复用同一套条件描述。配合
条件描述 "-v2" 系列项目使用
## 这个分支有什么 ## 这个仓库有什么
- `Condition`:条件树节点,`ConditionType``AND`/`OR`)是独立的 `string` 类型,`ToSql(m ### `Condition`/`ConditionExpr`
TokenValue) db.Expression` 内部把子条件/表达式各自渲染成字符串、`strings.Join` 拼接、手动
加括号、返回 `db.Raw(sql)`——不是表达式树,是纯字符串拼接。
- `ConditionExpr`:单个表达式(字段 + 操作符 + 值),`ConditionOperator` 是独立的 `string`
类型(`"="`/`"!="`/`"LIKE"`/`"IN"` 等字符串常量),`TokenValue` 接口定义在这个文件里
`GetParam(k string) req.GlobalParams`,不是新架构的 `valuex.Accessor`)。
- `engine/``Engine[T]` 规则引擎,`EngineParam` 包一层 `req.GlobalParams` 给条件求值用。
## 这个分支的定位 - `Condition`:条件树的节点,`AppendTo`/`SetCondition`/`SetExpr` 组装子条件/表达式,
`ToSql(m TokenValue) db.Expression` 渲染成 `db-v2` 的表达式树——内部用
`exp.NewExpressionList(this.typ, conditions...)` 构造,不是手工拼 SQL 字符串。
`ConditionType``exp.ExpressionListType``AND`/`OR`)的类型别名。
- `ConditionExpr`:单个表达式(字段 + 操作符 + 值),`ConditionOperator`
`exp.BooleanOperation` 的类型别名(`EQ`/`NE`/`GT`/`LIKE`/`IN` 等),跟 `db-v2` 的操作符
体系直接对齐,不用自己维护一份字符串常量再转换。
- `util.go``ToConditionOperator(op string) ConditionOperator` 把字符串(比如从配置/请求
参数解析出来的)转成 `ConditionOperator`
给还在维护、但还没迁移到新架构(`master` 分支上 `Condition.ToSql` 改用 `db-v2` 表达式树、 ### `TokenValue``token_value.go`
`TokenValue` 改用 `reflux/valuex`、新增 JSON 序列化支持)的老项目用,比如 `zg-estate`(间接
依赖)。只接受独立于新架构的 bug 修复。
## 依赖方怎么安装/更新 条件渲染时取值用的接口:`GetParam(k string) valuex.Accessor`(配合 `reflux/valuex` 统一属性
访问方式)+ `User() req.User``NewTokenValue(x valuex.Accessor, opts ...tValueOpt)` 构造,
`WithUser(user)` 显式指定用户;不传时会尝试从 `x` 上探测 `User()` 方法,两者都拿不到会
panic——`TokenValue` 必须绑定一个具体用户,不支持匿名/无用户场景。
Go modules 不会记住某个版本是从哪个分支解析出来的,`go.mod`/`go.sum` 里存的只是一次性解析出 ### JSON 序列化(`condition_json.go`
的 commit 伪版本号,分支上有新提交也不会自动同步。
**首次锁定,或者要拿这个分支上新提交的更新**,都执行同一条命令: `Condition`/`ConditionExpr` 都实现了 `MarshalJSON`/`UnmarshalJSON`(内部过一层 DTO 结构避免
递归),`FromMap(m map[string]any) (*Condition, error)` 直接从已解析好的 map 构造,不用先转
成 JSON 字符串再解析一遍——典型场景是权限配置、规则条件存在数据库的 JSON 列里,读出来直接
反序列化成 `Condition` 用。
``` ### `engine` 子包:规则引擎
go get git.fsdpf.net/go/condition@v1-legacy
```
**禁止**跑裸的 `go get -u`(或者 `go get git.fsdpf.net/go/condition@latest`)——这个仓库没有 `Engine[T any]`:按 `Case(cond *condition.Condition, cb func(data T, g req.GlobalParams) error)`
打语义化 tagGo 对"最新版本"的解析规则是退回到仓库默认分支(`master`)的 HEAD,会把新架构 注册一组"条件 -> 回调"规则,`Execute(data T)` 对传入数据求值,命中第一个条件为真的分支就
的破坏性改动一起拉进来,而不是停留在这个分支上。同时注意这条老线依赖的 调用对应回调(`Default(cb)` 兜底)。内部通过 `db/engine`sqlite3 内存库)把 `data`(用
`git.fsdpf.net/go/db`/`git.fsdpf.net/go/req` 也要一起锁定在它们各自的 `v1-legacy` 分支, `reflux.R` 包装)当成一行虚拟数据跑 SQL 判断条件是否成立,不用自己写一套表达式求值器。
不能新老混用 `EngineOption``Debug()`/`Relation(...)`)控制调试日志和关联字段展开
## 已知问题
`engine.go` 在调用方没有显式传 `GlobalParams` 时,会用 `nil` user 构造一个兜底值,这跟
`TokenValue` 现在强制要求非 nil user 冲突,`TestEngine` 目前会 panic——待 `Engine.Execute`
支持显式传 user 时一并解决,暂时未修。
## 分支状态
`master` 是持续开发中的新架构,**跟老仓库 `v1-legacy` 分支不兼容**——`Condition.ToSql` 从手工
拼 SQL 字符串换成了 `db-v2` 表达式树,`TokenValue``req.GlobalParams` 换成了
`reflux/valuex``engine` 包的连接管理也换成了 `db/engine`。还在用老版 API 的老项目,参见
`v1-legacy` 分支。
+14 -22
View File
@@ -5,15 +5,16 @@ import (
"strings" "strings"
"git.fsdpf.net/go/db" "git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/utils" "git.fsdpf.net/go/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
type ConditionType string type ConditionType = exp.ExpressionListType
const ( const (
OR ConditionType = "OR" AND ConditionType = exp.AndType
AND ConditionType = "AND" OR ConditionType = exp.OrType
) )
type Condition struct { type Condition struct {
@@ -92,44 +93,35 @@ func (this Condition) IsAlwaysRight() bool {
return !lo.Some(flag, []bool{false}) return !lo.Some(flag, []bool{false})
} }
// 生成 SQL 语句 // ToSql 生成 SQL 语句
func (this Condition) ToSql(m TokenValue) db.Expression { func (this Condition) ToSql(m TokenValue) db.Expression {
conditions := []string{} conditions := []exp.Expression{}
// 表达式 // 表达式
for _, item := range this.exprs { for _, item := range this.exprs {
if item.IsIgnoreEmptyParma(m) { if item.IsIgnoreEmptyParma(m) {
continue continue
} }
conditions = append(conditions, string(item.ToSql(m))) conditions = append(conditions, item.ToSql(m))
} }
// 条件子集 // 条件子集
for _, item := range this.childrens { for _, item := range this.childrens {
conditions = append(conditions, string(item.ToSql(m))) conditions = append(conditions, item.ToSql(m))
} }
// 去除无效的表达式
conditions = lo.Filter(conditions, func(item string, _ int) bool { return item != "" })
// 组合 SQL
sql := strings.Join(conditions, " "+string(this.Type())+" ")
if sql == "" { if this.IsEmpty() {
if this.parent == nil && this.Type() == OR { if this.parent == nil && this.Type() == OR {
return db.Raw("false") return db.V(false)
} else if this.parent == nil && this.Type() == AND { } else if this.parent == nil && this.Type() == AND {
return db.Raw("true") return db.V(true)
} }
return db.Raw("") return nil
}
// 包裹 SQL, 避免语法表达错误
if this.parent == nil || this.Type() == OR {
sql = "(" + sql + ")"
} }
if this.describe != "" { if this.describe != "" {
sql += "/*" + this.describe + "*/" return db.L("? /* ? */", exp.NewExpressionList(this.typ, conditions...), db.L(this.describe))
} }
return db.Raw(sql) return exp.NewExpressionList(this.typ, conditions...)
} }
// 设置条件表达式 // 设置条件表达式
+94 -98
View File
@@ -7,13 +7,13 @@ import (
"strings" "strings"
"git.fsdpf.net/go/db" "git.fsdpf.net/go/db"
"git.fsdpf.net/go/req" "git.fsdpf.net/go/db/exp"
"github.com/samber/lo" "git.fsdpf.net/go/db/sqlgen"
"github.com/spf13/cast" "git.fsdpf.net/go/reflux/valuex"
) )
type TokenType string type TokenType string
type ConditionOperator string type ConditionOperator = exp.BooleanOperation
const ( const (
SQL TokenType = "sql" SQL TokenType = "sql"
@@ -23,29 +23,24 @@ const (
) )
const ( const (
IS_NULL ConditionOperator = "IS NULL" IS_NULL ConditionOperator = exp.IsOp
IS_NOT_NULL ConditionOperator = "IS NOT NULL" IS_NOT_NULL ConditionOperator = exp.IsNotOp
EQ ConditionOperator = "=" EQ ConditionOperator = exp.EqOp // =
NE ConditionOperator = "!=" NE ConditionOperator = exp.NeqOp // != or <>
GT ConditionOperator = ">" GT ConditionOperator = exp.GtOp // >
GE ConditionOperator = ">=" GE ConditionOperator = exp.GteOp // >=
LT ConditionOperator = "<" LT ConditionOperator = exp.LtOp // <
LE ConditionOperator = "<=" LE ConditionOperator = exp.LteOp // <=
LIKE ConditionOperator = "LIKE" LIKE ConditionOperator = exp.LikeOp // LIKE
NOT_LIKE ConditionOperator = "NOT LIKE" NOT_LIKE ConditionOperator = exp.NotLikeOp // NOT LIKE
IN ConditionOperator = "IN" IN ConditionOperator = exp.InOp // IN
NOT_IN ConditionOperator = "NOT IN" NOT_IN ConditionOperator = exp.NotInOp // NOT IN
REGEXP ConditionOperator = "REGEXP" REGEXP ConditionOperator = exp.RegexpILikeOp // REGEXP
NOT_REGEXP ConditionOperator = "NOT REGEXP" NOT_REGEXP ConditionOperator = exp.RegexpNotILikeOp // NOT REGEXP
) )
type ExprOption func(option *ConditionExpr) type ExprOption func(option *ConditionExpr)
type TokenValue interface {
GetParam(k string) req.GlobalParams
GetGlobalParamsUser() req.User
}
type ConditionExpr struct { type ConditionExpr struct {
parent *Condition parent *Condition
operator ConditionOperator operator ConditionOperator
@@ -81,66 +76,43 @@ func (this *ConditionExpr) AppendTo(c *Condition) {
} }
func (this ConditionExpr) ToSql(m TokenValue) db.Expression { func (this ConditionExpr) ToSql(m TokenValue) db.Expression {
first := "`" + this.fieldResource + "`.`" + this.field + "`" var first exp.Expression = db.T(this.fieldResource).Col(this.field)
if strings.Contains(this.field, "->") { if strings.Contains(this.field, "->") {
first = "`" + this.fieldResource + "`." + this.field + "" first = db.L("?."+this.field+"", db.T(this.fieldResource))
} }
value := this.GetTokenSqlValue(m) var value db.Expression = this.GetTokenSqlValue(m)
operator := ConditionOperator(strings.ToUpper(string(this.GetOperator())))
if value == "" {
// @todo return true
// value = "''"
}
//
secondary := ""
switch operator {
case IS_NULL:
case IS_NOT_NULL:
secondary = ""
case EQ, NE,
GT, GE,
LT, LE,
REGEXP, NOT_REGEXP:
if this.GetTokenType() == SQL {
secondary = value
} else {
secondary = "'" + strings.Trim(value, "'") + "'"
}
switch this.GetOperator() {
case LIKE, NOT_LIKE: case LIKE, NOT_LIKE:
secondary = "'%" + strings.Trim(value, "'") + "%'" value = exp.NewSQLFunctionExpression("CONCAT", "%", value, "%")
case IN, NOT_IN:
secondary = "(" + lo.Ternary(value == "", "''", value) + ")"
} }
if this.fieldSqlFunc == "json_member_of" { if this.fieldSqlFunc == "" {
if this.fieldSqlFuncParam == "" { return exp.NewBooleanExpression(this.GetOperator(), first, value)
return db.Raw(fmt.Sprintf("JSON_CONTAINS(%s, JSON_ARRAY(%s))", secondary, first))
// return db.Raw(fmt.Sprintf("%s MEMBER OF(%s)", secondary, first))
} else {
return db.Raw(fmt.Sprintf("JSON_CONTAINS(%s->>'%s', JSON_ARRAY(%s))", secondary, this.fieldSqlFuncParam, first))
// return db.Raw(fmt.Sprintf("%s MEMBER OF(%s->'%s')", secondary, first, this.FieldSqlFuncParam))
}
} else if this.fieldSqlFunc == "json_contains" {
if this.fieldSqlFuncParam == "" {
return db.Raw(fmt.Sprintf("JSON_CONTAINS(%s, JSON_ARRAY(%s))", first, secondary))
} else {
return db.Raw(fmt.Sprintf("JSON_CONTAINS(%s->>'%s', JSON_ARRAY(%s))", first, this.fieldSqlFuncParam, secondary))
}
} else if this.fieldSqlFunc != "" && this.fieldSqlFuncParam != "" {
first = this.fieldSqlFunc + "(" + first + ", " + this.fieldSqlFuncParam + ")"
} else if this.fieldSqlFunc != "" {
first = this.fieldSqlFunc + "(" + first + ")"
} }
return db.Raw(strings.Trim(first+" "+string(operator)+" "+secondary, " ")) switch strings.ToLower(this.fieldSqlFunc) {
case "json_member_of":
if this.fieldSqlFuncParam != "" {
return exp.NewSQLFunctionExpression("JSON_CONTAINS", db.L(this.fieldSqlFuncParam, value), exp.NewSQLFunctionExpression("JSON_ARRAY", first))
}
return exp.NewSQLFunctionExpression("JSON_CONTAINS", value, exp.NewSQLFunctionExpression("JSON_ARRAY", first))
case "json_contains":
if this.fieldSqlFuncParam != "" {
return exp.NewSQLFunctionExpression("JSON_CONTAINS", db.L(this.fieldSqlFuncParam, first), exp.NewSQLFunctionExpression("JSON_ARRAY", value))
}
return exp.NewSQLFunctionExpression("JSON_CONTAINS", first, exp.NewSQLFunctionExpression("JSON_ARRAY", value))
}
if this.fieldSqlFuncParam == "" {
first = exp.NewSQLFunctionExpression(this.fieldSqlFunc, first)
} else {
first = exp.NewSQLFunctionExpression(this.fieldSqlFunc, first, db.L(this.fieldSqlFuncParam))
}
return exp.NewBooleanExpression(this.GetOperator(), first, value)
} }
func (this ConditionExpr) GetTokenName() string { func (this ConditionExpr) GetTokenName() string {
@@ -151,12 +123,17 @@ func (this ConditionExpr) GetTokenType() TokenType {
return this.tokenType return this.tokenType
} }
func (this *ConditionExpr) GetTokenSqlValue(m TokenValue) string { func (this *ConditionExpr) GetTokenSqlValue(m TokenValue) exp.LiteralExpression {
if this.GetTokenType() == SQL { if this.GetTokenType() == SQL {
return this.token return db.L(this.token)
} }
rv := reflect.ValueOf(this.GetTokenValue(m)) if this.operator == IS_NULL || this.operator == IS_NOT_NULL {
return db.V(nil)
}
v := this.GetTokenValue(m)
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr { if rv.Kind() == reflect.Ptr {
rv = reflect.Indirect(rv) rv = reflect.Indirect(rv)
@@ -164,51 +141,54 @@ func (this *ConditionExpr) GetTokenSqlValue(m TokenValue) string {
switch rv.Kind() { switch rv.Kind() {
case reflect.Invalid: case reflect.Invalid:
return "" return db.V("")
case reflect.Slice: case reflect.Slice:
aStr := cast.ToStringSlice(rv.Interface())
for i := 0; i < len(aStr); i++ {
if aStr[i] != "" {
aStr[i] = "'" + strings.Trim(db.MysqlRealEscapeString(aStr[i]), "'") + "'"
}
}
// 强制使用 in // 强制使用 in
if this.operator == EQ { if this.operator == EQ {
this.operator = IN this.operator = IN
} }
return db.V(v)
return strings.Join(aStr, ", ") case reflect.String:
if this.operator == IN || this.operator == NOT_IN {
parts := strings.Split(rv.String(), ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
return db.V(parts)
}
case reflect.Struct: case reflect.Struct:
b, _ := json.Marshal(rv.Interface()) b, _ := json.Marshal(v)
return fmt.Sprintf("'%s'", b) return db.V(b)
default:
return db.MysqlRealEscapeString(fmt.Sprintf("%v", rv.Interface()))
} }
return db.V(v)
} }
func (this ConditionExpr) GetTokenValue(m TokenValue) any { func (this ConditionExpr) GetTokenValue(m TokenValue) any {
switch this.GetTokenType() { switch this.GetTokenType() {
case PARAM: case PARAM:
var result valuex.Accessor
if this.matchPrefix != "" { if this.matchPrefix != "" {
return m.GetParam(fmt.Sprintf("%s.%s", this.matchPrefix, this.token)).Value() result = m.GetParam(fmt.Sprintf("%s.%s", this.matchPrefix, this.token))
} else {
result = m.GetParam(this.token)
} }
return m.GetParam(this.token).Value()
return result.Any()
case STRING: case STRING:
return this.token return this.token
case FUNC: case FUNC:
switch this.token { switch this.token {
case "UserID": case "UserID":
return m.GetGlobalParamsUser().ID() return m.User().ID()
case "UserUuid": case "UserUuid":
return m.GetGlobalParamsUser().Uuid() return m.User().Uuid()
case "UserRolesUuid": case "UserRolesUuid":
return m.GetGlobalParamsUser().Roles() return m.User().Roles()
case "UserPlatform": case "UserPlatform":
return m.GetGlobalParamsUser().Runtime().Platform() return m.User().Runtime().Platform()
case "UserSaaS": case "UserSaaS":
return m.GetGlobalParamsUser().Runtime().SaaS() return m.User().Runtime().SaaS()
} }
default: default:
return nil return nil
@@ -247,6 +227,12 @@ func Operator(v ConditionOperator) ExprOption {
} }
} }
func StringOperator(op string) ExprOption {
return func(option *ConditionExpr) {
option.operator = ToConditionOperator(op)
}
}
func Token(token string, tType TokenType) ExprOption { func Token(token string, tType TokenType) ExprOption {
return func(option *ConditionExpr) { return func(option *ConditionExpr) {
option.token = token option.token = token
@@ -281,3 +267,13 @@ func NewExpr(rResource, rField string, opts ...ExprOption) *ConditionExpr {
expr.SetOption(opts...) expr.SetOption(opts...)
return expr return expr
} }
func GetDBOperator(op string) ConditionOperator {
ops := sqlgen.DefaultDialectOptions().BooleanOperatorLookup
for t, v := range ops {
if string(v) == strings.ToUpper(op) {
return t
}
}
return exp.EqOp
}
+161
View File
@@ -0,0 +1,161 @@
package condition
import (
"encoding/json"
"strings"
)
// conditionDTO 是 Condition 的 JSON 中间结构,用于避免 UnmarshalJSON 递归
type conditionDTO struct {
Type string `json:"type"`
Describe string `json:"describe,omitempty"`
Exprs []*exprDTO `json:"exprs,omitempty"`
Childrens []*conditionDTO `json:"childrens,omitempty"`
}
type exprDTO struct {
Operator string `json:"operator"`
Field string `json:"field"`
FieldResource string `json:"fieldResource"`
FieldSqlFunc string `json:"fieldSqlFunc,omitempty"`
FieldSqlFuncParam string `json:"fieldSqlFuncParam,omitempty"`
IgnoreEmptyParam bool `json:"ignoreEmptyParam,omitempty"`
TokenType TokenType `json:"tokenType"`
Token string `json:"token"`
}
func (this *Condition) UnmarshalJSON(data []byte) error {
var dto conditionDTO
if err := json.Unmarshal(data, &dto); err != nil {
return err
}
conditionFromDTO(this, &dto)
return nil
}
func (this Condition) MarshalJSON() ([]byte, error) {
return json.Marshal(conditionToDTO(&this))
}
func (this *ConditionExpr) UnmarshalJSON(data []byte) error {
var dto exprDTO
if err := json.Unmarshal(data, &dto); err != nil {
return err
}
*this = *exprFromDTO(&dto)
return nil
}
func (this ConditionExpr) MarshalJSON() ([]byte, error) {
return json.Marshal(exprToDTO(&this))
}
// FromMap 将 map[string]any 转换为 *Condition
func FromMap(m map[string]any) (*Condition, error) {
data, err := json.Marshal(m)
if err != nil {
return nil, err
}
cond := New()
if err := json.Unmarshal(data, cond); err != nil {
return nil, err
}
return cond, nil
}
func conditionFromDTO(c *Condition, dto *conditionDTO) {
if strings.EqualFold(dto.Type, "OR") {
c.typ = OR
} else {
c.typ = AND
}
c.describe = dto.Describe
for _, e := range dto.Exprs {
c.SetExpr(exprFromDTO(e))
}
for _, child := range dto.Childrens {
childCond := &Condition{}
conditionFromDTO(childCond, child)
c.SetCondition(childCond)
}
}
func conditionToDTO(c *Condition) *conditionDTO {
typ := "AND"
if c.typ == OR {
typ = "OR"
}
dto := &conditionDTO{Type: typ, Describe: c.describe}
for _, e := range c.exprs {
dto.Exprs = append(dto.Exprs, exprToDTO(e))
}
for _, child := range c.childrens {
dto.Childrens = append(dto.Childrens, conditionToDTO(child))
}
return dto
}
func exprFromDTO(dto *exprDTO) *ConditionExpr {
tokenType := dto.TokenType
if tokenType == "" {
tokenType = STRING
}
return &ConditionExpr{
operator: ToConditionOperator(dto.Operator),
field: dto.Field,
fieldResource: dto.FieldResource,
fieldSqlFunc: dto.FieldSqlFunc,
fieldSqlFuncParam: dto.FieldSqlFuncParam,
ignoreEmptyParma: dto.IgnoreEmptyParam,
tokenType: tokenType,
token: dto.Token,
}
}
func exprToDTO(e *ConditionExpr) *exprDTO {
return &exprDTO{
Operator: operatorToString(e.operator),
Field: e.field,
FieldResource: e.fieldResource,
FieldSqlFunc: e.fieldSqlFunc,
FieldSqlFuncParam: e.fieldSqlFuncParam,
IgnoreEmptyParam: e.ignoreEmptyParma,
TokenType: e.tokenType,
Token: e.token,
}
}
func operatorToString(op ConditionOperator) string {
switch op {
case IS_NULL:
return "IS NULL"
case IS_NOT_NULL:
return "IS NOT NULL"
case EQ:
return "="
case NE:
return "!="
case GT:
return ">"
case GE:
return ">="
case LT:
return "<"
case LE:
return "<="
case LIKE:
return "LIKE"
case NOT_LIKE:
return "NOT LIKE"
case IN:
return "IN"
case NOT_IN:
return "NOT IN"
case REGEXP:
return "REGEXP"
case NOT_REGEXP:
return "NOT REGEXP"
default:
return "="
}
}
+126
View File
@@ -0,0 +1,126 @@
package condition_test
import (
"encoding/json"
"testing"
"git.fsdpf.net/go/condition"
"github.com/stretchr/testify/suite"
)
type conditionJSONTest struct {
suite.Suite
}
func TestConditionJSONSuite(t *testing.T) {
suite.Run(t, new(conditionJSONTest))
}
// 构造一个有代表性的多层条件
func buildTestCondition() *condition.Condition {
cond := condition.New(condition.Describe("测试"))
cond.SetExpr(condition.NewExpr("User", "name",
condition.Operator(condition.LIKE),
condition.Token("张三", condition.STRING),
))
cond.SetExpr(condition.NewExpr("User", "age",
condition.Operator(condition.GE),
condition.Token("18", condition.STRING),
))
sub := condition.New(condition.Type(condition.OR))
sub.SetExpr(condition.NewExpr("User", "status",
condition.Operator(condition.IN),
condition.Token("active", condition.STRING),
))
sub.SetExpr(condition.NewExpr("User", "roles",
condition.FieldSqlFn("json_contains", ""),
condition.Token("admin", condition.STRING),
))
cond.SetCondition(sub)
return cond
}
func (t *conditionJSONTest) TestMarshalUnmarshal() {
orig := buildTestCondition()
data, err := json.Marshal(orig)
t.Require().NoError(err)
t.T().Log("JSON:", string(data))
got := condition.New()
t.Require().NoError(json.Unmarshal(data, got))
// 二次序列化结果应一致
data2, err := json.Marshal(got)
t.Require().NoError(err)
t.Equal(string(data), string(data2))
}
func (t *conditionJSONTest) TestFromMap() {
m := map[string]any{
"type": "AND",
"describe": "来自Map",
"exprs": []any{
map[string]any{
"operator": "=",
"field": "name",
"fieldResource": "User",
"tokenType": "string",
"token": "张三",
},
map[string]any{
"operator": ">=",
"field": "age",
"fieldResource": "User",
"tokenType": "string",
"token": "18",
},
},
"childrens": []any{
map[string]any{
"type": "OR",
"exprs": []any{
map[string]any{
"operator": "IS NULL",
"field": "deleted_at",
"fieldResource": "User",
"tokenType": "string",
"token": "",
},
},
},
},
}
cond, err := condition.FromMap(m)
t.Require().NoError(err)
t.False(cond.IsEmpty())
data, err := json.Marshal(cond)
t.Require().NoError(err)
t.T().Log("FromMap JSON:", string(data))
}
func (t *conditionJSONTest) TestRoundTrip() {
orig := buildTestCondition()
// Condition → JSON → Condition → JSON,两次 JSON 应一致
data1, err := json.Marshal(orig)
t.Require().NoError(err)
cond2 := condition.New()
t.Require().NoError(json.Unmarshal(data1, cond2))
data2, err := json.Marshal(cond2)
t.Require().NoError(err)
t.JSONEq(string(data1), string(data2))
}
func (t *conditionJSONTest) TestFromMapEmpty() {
cond, err := condition.FromMap(map[string]any{})
t.Require().NoError(err)
t.True(cond.IsEmpty())
}
+41
View File
@@ -0,0 +1,41 @@
package condition_test
import (
"testing"
"git.fsdpf.net/go/condition"
"git.fsdpf.net/go/db"
"github.com/stretchr/testify/suite"
_ "git.fsdpf.net/go/db/dialect/sqlite3"
db_eng "git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/db/sqlgen"
)
type conditionTest struct {
suite.Suite
db *db.Database
esg sqlgen.ExpressionSQLGenerator
}
func TestConditionSuite(t *testing.T) {
suite.Run(t, new(conditionTest))
}
func (t *conditionTest) SetupSuite() {
database := db_eng.Open(map[string]db_eng.DBConfig{
"condition-test": db_eng.NewDBConfig("sqlite3", db_eng.WithSQLiteFile(":memory:")),
})
t.db = database.Connection("condition-test")
}
func (t *conditionTest) TestToSql() {
cond := condition.New(condition.Describe("测试"))
cond.SetExpr(condition.NewExpr("User", "name", condition.Operator(condition.LIKE), condition.Token("张三", condition.STRING))).
SetExpr(condition.NewExpr("User", "age", condition.Token("18", condition.STRING))).
SetExpr(condition.NewExpr("User", "attr->'birthday'", condition.Token("18", condition.STRING))).
SetExpr(condition.NewExpr("User", "roles", condition.FieldSqlFn("json_contains", ""), condition.Token("admin", condition.STRING)))
// SetExpr(condition.NewExpr("User", "roles", condition.FieldSqlFn("json_contains", ""), condition.Token("admin", condition.STRING)))
t.T().Log(t.db.From(db.T("users").As("User")).Where(cond.ToSql(nil)).ToSQL())
}
+71 -70
View File
@@ -4,16 +4,21 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strconv"
"git.fsdpf.net/go/condition" "git.fsdpf.net/go/condition"
"git.fsdpf.net/go/db" "git.fsdpf.net/go/db"
_ "git.fsdpf.net/go/db/dialect/sqlite3"
db_eng "git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/req" "git.fsdpf.net/go/req"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
var conn *db.Connection var conn *db.Database
var defaultEngineOptions = engineOptions{ var defaultEngineOptions = engineOptions{
debug: false, debug: false,
@@ -21,76 +26,73 @@ var defaultEngineOptions = engineOptions{
} }
func init() { func init() {
database := db.Open(map[string]db.DBConfig{ database := db_eng.Open(map[string]db_eng.DBConfig{
"condition-engine-sqlite3": { "condition-engine-sqlite3": db_eng.NewDBConfig("sqlite3", db_eng.WithSQLiteFile(":memory:")),
Driver: "sqlite3",
File: ":memory:",
},
}) })
conn = database.Connection("condition-engine-sqlite3") conn = database.Connection("condition-engine-sqlite3")
} }
type Engine[T any] struct { type Engine struct {
code string code string
opts engineOptions opts engineOptions
g req.GlobalParams g req.UserAccessor
def func(data T, g req.GlobalParams) error def func(data reflux.R, g req.UserAccessor) error
predicates []*EngineCase[T] predicates []*EngineCase
} }
func (this Engine[T]) GetCode() string { func (this Engine) GetCode() string {
return this.code return this.code
} }
// 公共参数 // 公共参数
func (this *Engine[T]) SetGlobalParams(g req.GlobalParams) *Engine[T] { func (this *Engine) SetGlobalParams(g req.UserAccessor) *Engine {
this.g = g this.g = g
return this return this
} }
func (this *Engine[T]) Case(cond *condition.Condition, cb func(data T, g req.GlobalParams) error) *Engine[T] { func (this *Engine) Case(cond *condition.Condition, cb func(data reflux.R, g req.UserAccessor) error) *Engine {
this.predicates = append(this.predicates, &EngineCase[T]{cond, cb}) this.predicates = append(this.predicates, &EngineCase{cond, cb})
return this return this
} }
// 基础条件 // 基础条件
func (this *Engine[T]) Default(cb func(data T, g req.GlobalParams) error) *Engine[T] { func (this *Engine) Default(cb func(data reflux.R, g req.UserAccessor) error) *Engine {
this.def = cb this.def = cb
return this return this
} }
func (this Engine[T]) toField(rv reflect.Value) (string, error) { func (this Engine) toField(rv reflect.Value, as string) (db.Expression, error) {
if rv.IsZero() {
return db.V(nil).As(as), nil
}
if rv.Kind() == reflect.Interface {
rv = rv.Elem()
}
if rv.Kind() == reflect.Ptr { if rv.Kind() == reflect.Ptr {
rv = reflect.Indirect(rv) rv = reflect.Indirect(rv)
} }
if rv.IsZero() {
return "NULL", nil
}
v := rv.Interface() v := rv.Interface()
switch reflect.TypeOf(v).Kind() { switch reflect.TypeOf(v).Kind() {
case reflect.Map, reflect.Array, reflect.Slice, reflect.Struct: case reflect.Map, reflect.Array, reflect.Slice, reflect.Struct:
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil { if err != nil {
return "", err return exp.NewSQLFunctionExpression("JSON", nil).As(as), err
} }
return fmt.Sprintf("CAST('%s' AS JSON1)", b), nil return exp.NewSQLFunctionExpression("JSON", string(b)).As(as), err
case reflect.String: case reflect.String:
return fmt.Sprintf("CAST('%s' AS TEXT)", v), nil return db.Cast(db.V(v), "TEXT").As(as), nil
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int8, reflect.Int16, case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int8, reflect.Int16,
reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint8, reflect.Uint16: reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint8, reflect.Uint16:
return fmt.Sprintf("CAST(%d AS INTEGER)", v), nil return db.Cast(db.V(v), "INTEGER").As(as), nil
case reflect.Float64, reflect.Float32: case reflect.Float64, reflect.Float32:
return fmt.Sprintf("CAST(%v AS REAL)", v), nil return db.Cast(db.V(v), "REAL").As(as), nil
case reflect.Bool: case reflect.Bool:
return fmt.Sprintf("CAST(%v AS BOOL)", v), nil return db.Cast(db.V(v), "BOOL").As(as), nil
} }
return "NULL", nil return db.V(nil).As(as), nil
} }
func (this Engine[T]) toTables(rv reflect.Value, table string) (tables map[string][]string, err error) { func (this Engine) toTables(rv reflect.Value, table string) (tables map[string][]db.Expression, err error) {
rt := rv.Type() rt := rv.Type()
if rv.Kind() == reflect.Ptr { if rv.Kind() == reflect.Ptr {
@@ -99,11 +101,11 @@ func (this Engine[T]) toTables(rv reflect.Value, table string) (tables map[strin
} }
if tables == nil { if tables == nil {
tables = map[string][]string{} tables = map[string][]db.Expression{}
} }
if _, ok := tables[table]; !ok { if _, ok := tables[table]; !ok {
tables[table] = []string{} tables[table] = []db.Expression{}
} }
if rv.Kind() == reflect.Struct { if rv.Kind() == reflect.Struct {
@@ -115,10 +117,10 @@ func (this Engine[T]) toTables(rv reflect.Value, table string) (tables map[strin
} else { } else {
return nil, err return nil, err
} }
} else if s, err := this.toField(rv.Field(i)); err != nil { } else if col, err := this.toField(rv.Field(i), lo.Ternary(rt.Field(i).Tag.Get("db") != "", rt.Field(i).Tag.Get("db"), rt.Field(i).Name)); err != nil {
return nil, err return nil, err
} else { } else {
tables[table] = append(tables[table], fmt.Sprintf("%s as `%s`", s, lo.Ternary(rt.Field(i).Tag.Get("db") != "", rt.Field(i).Tag.Get("db"), rt.Field(i).Name))) tables[table] = append(tables[table], col)
} }
} }
} else if rv.Kind() == reflect.Map { } else if rv.Kind() == reflect.Map {
@@ -130,10 +132,10 @@ func (this Engine[T]) toTables(rv reflect.Value, table string) (tables map[strin
} else { } else {
return nil, err return nil, err
} }
} else if s, err := this.toField(iter.Value()); err != nil { } else if col, err := this.toField(iter.Value(), iter.Key().String()); err != nil {
return nil, err return nil, err
} else { } else {
tables[table] = append(tables[table], fmt.Sprintf("%s as `%s`", s, iter.Key().String())) tables[table] = append(tables[table], col)
} }
} }
} else { } else {
@@ -143,66 +145,61 @@ func (this Engine[T]) toTables(rv reflect.Value, table string) (tables map[strin
return tables, nil return tables, nil
} }
func (this *Engine[T]) Execute(data T) error { func (this *Engine) Execute(data any) error {
rfx := reflux.New(data)
tables, err := this.toTables(reflect.ValueOf(data), this.GetCode()) tables, err := this.toTables(rfx.Raw(), this.GetCode())
if err != nil { if err != nil {
return err return err
} }
columns, ok := tables[this.GetCode()] ts := []any{}
// 加载数据
if !ok { for table, columns := range tables {
return fmt.Errorf("data is not") ts = append(ts, db.Select(this.toSelectCols(columns)...).As(table))
}
sql := conn.Query().FromSub(fmt.Sprintf("SELECT %s", strings.Join(append(columns, "1 as `_`"), ", ")), this.GetCode())
for table, columns := range lo.OmitByKeys(tables, []string{this.GetCode()}) {
sql.JoinSub(
fmt.Sprintf("SELECT %s", strings.Join(append(columns, "1 as `_`"), ", ")),
table,
fmt.Sprintf("%s._", this.GetCode()),
fmt.Sprintf("%s._", table),
)
} }
_db := conn.From(ts...)
if this.g == nil { if this.g == nil {
this.g = req.NewGlobalParam(`{}`, nil) this.g = req.NewGlobalParams(`{}`, nil)
} }
param := &EngineParam{this.g} param := condition.NewTokenValue(this.g)
cols := []any{}
// 加载条件
for i, p := range this.predicates { for i, p := range this.predicates {
sql.AddSelect(db.Raw(fmt.Sprintf("IFNULL(%s, 0) as `%d`", p.ToSql(param), i))) cols = append(cols, exp.NewSQLFunctionExpression("IFNULL", p.ToSql(param), 0).As(strconv.Itoa(i)))
} }
_db = _db.Select(cols...)
result := map[string]int64{}
if this.opts.debug { if this.opts.debug {
fmt.Println("sql", sql.ToSql()) fmt.Println(_db.ToSQL())
} }
if _, err := sql.First(&result); err != nil { result, err := _db.Executor().GetRecord()
return fmt.Errorf("%s => %s", err, sql.ToSql()) if err != nil {
sql, _, _ := _db.ToSQL()
return fmt.Errorf("%s => %s", err, sql)
} }
isDefault := true isDefault := true
for k, v := range result { for fnIndex, v := range result {
if v == 0 { b := cast.ToInt64(v)
if b == 0 {
continue continue
} }
isDefault = false isDefault = false
if err := this.predicates[cast.ToInt64(k)].Execute(data, this.g); err != nil { if err := this.predicates[cast.ToInt64(fnIndex)].Execute(rfx, this.g); err != nil {
return fmt.Errorf("case %q error, %s", k, err) return fmt.Errorf("case %q error, %s", fnIndex, err)
} }
} }
if isDefault && this.def != nil { if isDefault && this.def != nil {
if err := this.def(data, this.g); err != nil { if err := this.def(rfx, this.g); err != nil {
return fmt.Errorf("case %q error, %s", "default", err) return fmt.Errorf("case %q error, %s", "default", err)
} }
} }
@@ -210,16 +207,20 @@ func (this *Engine[T]) Execute(data T) error {
return nil return nil
} }
func New[T any](table string, opt ...EngineOption) *Engine[T] { func (this *Engine) toSelectCols(cols []db.Expression) []any {
return lo.Map(cols, func(item db.Expression, _ int) any { return item })
}
func New(table string, opt ...EngineOption) *Engine {
opts := defaultEngineOptions opts := defaultEngineOptions
for _, o := range opt { for _, o := range opt {
o.apply(&opts) o.apply(&opts)
} }
return &Engine[T]{ return &Engine{
code: table, code: table,
opts: opts, opts: opts,
g: req.NewGlobalParam("", nil), // g: req.NewParam("", nil),
} }
} }
+6 -5
View File
@@ -3,22 +3,23 @@ package engine
import ( import (
"git.fsdpf.net/go/condition" "git.fsdpf.net/go/condition"
"git.fsdpf.net/go/db" "git.fsdpf.net/go/db"
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/req" "git.fsdpf.net/go/req"
) )
type EngineCase[T any] struct { type EngineCase struct {
predicate *condition.Condition predicate *condition.Condition
cb func(data T, g req.GlobalParams) error cb func(data reflux.R, g req.UserAccessor) error
} }
func (this EngineCase[T]) ToSql(param condition.TokenValue) db.Expression { func (this EngineCase) ToSql(param condition.TokenValue) db.Expression {
if this.predicate == nil || this.predicate.IsEmpty() { if this.predicate == nil || this.predicate.IsEmpty() {
return db.Raw("NULL") return db.V("NULL")
} }
return this.predicate.ToSql(param) return this.predicate.ToSql(param)
} }
func (this EngineCase[T]) Execute(data T, g req.GlobalParams) error { func (this EngineCase) Execute(data reflux.R, g req.UserAccessor) error {
return this.cb(data, g) return this.cb(data, g)
} }
-17
View File
@@ -1,17 +0,0 @@
package engine
import (
"git.fsdpf.net/go/req"
)
type EngineParam struct {
data req.GlobalParams
}
func (this EngineParam) GetParam(k string) req.GlobalParams {
return this.data.Get(k)
}
func (this EngineParam) GetGlobalParamsUser() req.User {
return this.data.User()
}
+35 -20
View File
@@ -4,6 +4,7 @@ import (
"testing" "testing"
"git.fsdpf.net/go/condition" "git.fsdpf.net/go/condition"
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/req" "git.fsdpf.net/go/req"
) )
@@ -18,26 +19,40 @@ type TestTable struct {
} }
func TestEngine(t *testing.T) { func TestEngine(t *testing.T) {
engine := Engine[any]{ engine := Engine{
code: "TestTable", code: "TestTable",
g: req.NewGlobalParam(`{"age": 30}`, nil), g: req.NewGlobalParams(`{"age": 30}`, nil),
} }
cond1 := condition.New(condition.Describe("条件1")) cond1 := condition.New(condition.Describe("条件1"))
cond1.SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM))) cond1.SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM)))
engine.Case(cond1, func(data any, g req.GlobalParams) error { engine.Case(cond1, func(data reflux.R, g req.UserAccessor) error {
t.Log("cond1", data) t.Log("cond1", data.Any())
return nil return nil
}) })
engine.Default(func(data any, g req.GlobalParams) error { engine.Default(func(data reflux.R, g req.UserAccessor) error {
t.Log("default", data) t.Log("default", data.Any())
return nil return nil
}) })
engine.Execute(map[string]any{ testData := map[string]any{
"null": nil,
"age": 30,
"name": "张三",
"map": map[string]any{
"field": 1,
},
"bool": false,
"struct": struct{ Name string }{"李四"},
"struct-ptr": &struct{ Name string }{"张三"},
"array": []any{1, "a", false},
}
engine.Execute(testData)
testData2 := map[string]any{
"null": nil, "null": nil,
"age": 30, "age": 30,
"name": "张三", "name": "张三",
@@ -47,15 +62,14 @@ func TestEngine(t *testing.T) {
"bool": false, "bool": false,
"struct": &struct{ Name string }{"李四"}, "struct": &struct{ Name string }{"李四"},
"array": []any{1, "a", false}, "array": []any{1, "a", false},
}) }
t.Log("execute error", engine.Execute(testData2))
t.Log("execute error", engine.Execute(TestTable{nil, 30, "张三", map[string]any{"field": 1}, false, &struct{ Name string }{"李四"}, []any{1, "a", false}}))
} }
func TestRelationEngine(t *testing.T) { func TestRelationEngine(t *testing.T) {
engine := Engine[any]{ engine := Engine{
code: "TestTable", code: "TestTable",
g: req.NewGlobalParam(`{"age": 30}`, nil), g: req.NewGlobalParams(`{"age": 30}`, nil),
opts: engineOptions{ opts: engineOptions{
debug: false, debug: false,
relations: []string{"TestTableA"}, relations: []string{"TestTableA"},
@@ -65,8 +79,8 @@ func TestRelationEngine(t *testing.T) {
cond1 := condition.New(condition.Describe("条件1")). cond1 := condition.New(condition.Describe("条件1")).
SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM))) SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM)))
engine.Case(cond1, func(data any, g req.GlobalParams) error { engine.Case(cond1, func(data reflux.R, g req.UserAccessor) error {
t.Log("cond1", data) t.Log("cond1", data.Any())
return nil return nil
}) })
@@ -74,17 +88,17 @@ func TestRelationEngine(t *testing.T) {
SetExpr(condition.NewExpr("TestTableA", "age", condition.Token("age", condition.PARAM))). SetExpr(condition.NewExpr("TestTableA", "age", condition.Token("age", condition.PARAM))).
SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM))) SetExpr(condition.NewExpr("TestTable", "age", condition.Token("age", condition.PARAM)))
engine.Case(cond2, func(data any, g req.GlobalParams) error { engine.Case(cond2, func(data reflux.R, g req.UserAccessor) error {
t.Log("cond2", data) t.Log("cond2", data.Any())
return nil return nil
}) })
engine.Default(func(data any, g req.GlobalParams) error { engine.Default(func(data reflux.R, g req.UserAccessor) error {
t.Log("default", data) t.Log("default", data.Any())
return nil return nil
}) })
engine.Execute(map[string]any{ testData := map[string]any{
"null": nil, "null": nil,
"age": 31, "age": 31,
"name": "张三", "name": "张三",
@@ -100,7 +114,8 @@ func TestRelationEngine(t *testing.T) {
"age": 22, "age": 22,
}, },
}, },
}) }
engine.Execute(testData)
// t.Log("execute error", engine.Execute(TestTable{nil, 30, "张三", map[string]any{"field": 1}, false, &struct{ Name string }{"李四"}, []any{1, "a", false}})) // t.Log("execute error", engine.Execute(TestTable{nil, 30, "张三", map[string]any{"field": 1}, false, &struct{ Name string }{"李四"}, []any{1, "a", false}}))
} }
+12 -8
View File
@@ -1,22 +1,26 @@
module git.fsdpf.net/go/condition module git.fsdpf.net/go/condition
go 1.21 go 1.25.5
require ( require (
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff
github.com/samber/lo v1.39.0 github.com/samber/lo v1.49.1
github.com/spf13/cast v1.6.0 github.com/spf13/cast v1.10.0
github.com/stretchr/testify v1.11.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+37 -24
View File
@@ -1,35 +1,43 @@
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b h1:fRgWNOQ9dAYuUZHQ24oi1XqRbJIcJvZGbnQDaAKI7IY= git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508 h1:xv2SV/Qb0TKJFnzfBtSKSeXwa6NZPRBVoGfWDahsezo=
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w= git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508/go.mod h1:oDPmvCdZn/vTpbuPYPYkjdrhpis/XtMIi3eni4sYRvM=
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20 h1:PhjRtac2r0Vn1WBBzhRhz4//bJhHgkf/SHZe3mQYPto= git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1 h1:TRn++7oWwtANQM5JWTQGdW6fAkWMIbIsZX6tzmnsrLQ=
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1/go.mod h1:8OLCLMUwdsZ8u2sRI9y/qaE4fTf5d0SrCJ6Zn+lttFY=
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56 h1:7ElgXUzt75brzOr44mItipTQTk++kZNoGOyTWHLxkTU= git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0 h1:HWM1soZb13IIQA69M8DgyLYu9goz7PArtWI2mQay918=
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0/go.mod h1:Lm8WoA8EYs2696rmlxpySoCQY26Qh97cDxVF6NLBfZ8=
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60 h1:hHrEcUSbtajC2LAo2fvEo7yrSSDeM/s5FAuQluP7yZQ=
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 h1:nHys8Bwb29b/FvnE100KVH62aD2j5CEPxTQ/RRHo27k=
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff h1:1hokQGKGIstoI7WDBir2N24OGgnussZ3GmucK4TQRuI= git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff h1:1hokQGKGIstoI7WDBir2N24OGgnussZ3GmucK4TQRuI=
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff/go.mod h1:NUoyQtFr905YT+pi850mvSf4YX0WghQIcMQkTvize5o= git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff/go.mod h1:NUoyQtFr905YT+pi850mvSf4YX0WghQIcMQkTvize5o=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -39,5 +47,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+53
View File
@@ -0,0 +1,53 @@
package condition
import (
"git.fsdpf.net/go/reflux/valuex"
"git.fsdpf.net/go/req"
)
type TokenValue interface {
GetParam(k string) valuex.Accessor
User() req.User
}
type tValueOpt func(option *tValue)
func WithUser(user req.User) tValueOpt {
return func(option *tValue) {
option.user = user
}
}
func NewTokenValue(x valuex.Accessor, opts ...tValueOpt) TokenValue {
tv := &tValue{src: x}
if x == nil {
tv.src = valuex.Nil
} else if up, ok := x.(interface{ User() req.User }); ok {
tv.user = up.User()
}
for _, opt := range opts {
if opt != nil {
opt(tv)
}
}
if tv.user == nil {
panic("user is required")
}
return tv
}
type tValue struct {
src valuex.Accessor
user req.User
}
func (this *tValue) GetParam(k string) valuex.Accessor {
if v, ok := this.src.Lookup(k); ok {
return v
}
return valuex.Nil
}
func (this *tValue) User() req.User {
return this.user
}
+39
View File
@@ -0,0 +1,39 @@
package condition
import (
"strings"
)
func ToConditionOperator(op string) ConditionOperator {
switch strings.ToUpper(op) {
case "IS NULL":
return IS_NULL
case "IS NOT NULL":
return IS_NOT_NULL
case "=":
return EQ
case "!=", "<>":
return NE
case ">":
return GT
case ">=":
return GE
case "<":
return LT
case "<=":
return LE
case "LIKE":
return LIKE
case "NOT LIKE":
return NOT_LIKE
case "IN":
return IN
case "NOT IN":
return NOT_IN
case "REGEXP":
return REGEXP
case "NOT REGEXP":
return NOT_REGEXP
}
return EQ
}