Files
condition/engine/engine.go
T
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

227 lines
5.5 KiB
Go

package engine
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"git.fsdpf.net/go/condition"
"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"
"github.com/samber/lo"
"github.com/spf13/cast"
)
var conn *db.Database
var defaultEngineOptions = engineOptions{
debug: false,
relations: []string{},
}
func init() {
database := db_eng.Open(map[string]db_eng.DBConfig{
"condition-engine-sqlite3": db_eng.NewDBConfig("sqlite3", db_eng.WithSQLiteFile(":memory:")),
})
conn = database.Connection("condition-engine-sqlite3")
}
type Engine struct {
code string
opts engineOptions
g req.UserAccessor
def func(data reflux.R, g req.UserAccessor) error
predicates []*EngineCase
}
func (this Engine) GetCode() string {
return this.code
}
// 公共参数
func (this *Engine) SetGlobalParams(g req.UserAccessor) *Engine {
this.g = g
return this
}
func (this *Engine) Case(cond *condition.Condition, cb func(data reflux.R, g req.UserAccessor) error) *Engine {
this.predicates = append(this.predicates, &EngineCase{cond, cb})
return this
}
// 基础条件
func (this *Engine) Default(cb func(data reflux.R, g req.UserAccessor) error) *Engine {
this.def = cb
return this
}
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 {
rv = reflect.Indirect(rv)
}
v := rv.Interface()
switch reflect.TypeOf(v).Kind() {
case reflect.Map, reflect.Array, reflect.Slice, reflect.Struct:
b, err := json.Marshal(v)
if err != nil {
return exp.NewSQLFunctionExpression("JSON", nil).As(as), err
}
return exp.NewSQLFunctionExpression("JSON", string(b)).As(as), err
case reflect.String:
return db.Cast(db.V(v), "TEXT").As(as), nil
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int8, reflect.Int16,
reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint8, reflect.Uint16:
return db.Cast(db.V(v), "INTEGER").As(as), nil
case reflect.Float64, reflect.Float32:
return db.Cast(db.V(v), "REAL").As(as), nil
case reflect.Bool:
return db.Cast(db.V(v), "BOOL").As(as), nil
}
return db.V(nil).As(as), nil
}
func (this Engine) toTables(rv reflect.Value, table string) (tables map[string][]db.Expression, err error) {
rt := rv.Type()
if rv.Kind() == reflect.Ptr {
rv = reflect.Indirect(rv.Elem())
rt = rv.Type()
}
if tables == nil {
tables = map[string][]db.Expression{}
}
if _, ok := tables[table]; !ok {
tables[table] = []db.Expression{}
}
if rv.Kind() == reflect.Struct {
// 遍历结构体的字段
for i := 0; i < rt.NumField(); i++ {
if _, ok := tables[rt.Field(i).Name]; !ok && lo.Contains(this.opts.relations, rt.Field(i).Name) {
if items, err := this.toTables(rv.Field(i).Elem(), rt.Field(i).Name); err == nil {
tables = lo.Assign(tables, items)
} else {
return nil, err
}
} 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
} else {
tables[table] = append(tables[table], col)
}
}
} else if rv.Kind() == reflect.Map {
iter := rv.MapRange()
for iter.Next() {
if _, ok := tables[iter.Key().String()]; !ok && lo.Contains(this.opts.relations, iter.Key().String()) {
if items, err := this.toTables(iter.Value().Elem(), iter.Key().String()); err == nil {
tables = lo.Assign(tables, items)
} else {
return nil, err
}
} else if col, err := this.toField(iter.Value(), iter.Key().String()); err != nil {
return nil, err
} else {
tables[table] = append(tables[table], col)
}
}
} else {
return nil, fmt.Errorf("data type not map/struct, %s", rv.Kind())
}
return tables, nil
}
func (this *Engine) Execute(data any) error {
rfx := reflux.New(data)
tables, err := this.toTables(rfx.Raw(), this.GetCode())
if err != nil {
return err
}
ts := []any{}
// 加载数据
for table, columns := range tables {
ts = append(ts, db.Select(this.toSelectCols(columns)...).As(table))
}
_db := conn.From(ts...)
if this.g == nil {
this.g = req.NewGlobalParams(`{}`, nil)
}
param := condition.NewTokenValue(this.g)
cols := []any{}
// 加载条件
for i, p := range this.predicates {
cols = append(cols, exp.NewSQLFunctionExpression("IFNULL", p.ToSql(param), 0).As(strconv.Itoa(i)))
}
_db = _db.Select(cols...)
if this.opts.debug {
fmt.Println(_db.ToSQL())
}
result, err := _db.Executor().GetRecord()
if err != nil {
sql, _, _ := _db.ToSQL()
return fmt.Errorf("%s => %s", err, sql)
}
isDefault := true
for fnIndex, v := range result {
b := cast.ToInt64(v)
if b == 0 {
continue
}
isDefault = false
if err := this.predicates[cast.ToInt64(fnIndex)].Execute(rfx, this.g); err != nil {
return fmt.Errorf("case %q error, %s", fnIndex, err)
}
}
if isDefault && this.def != nil {
if err := this.def(rfx, this.g); err != nil {
return fmt.Errorf("case %q error, %s", "default", err)
}
}
return nil
}
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
for _, o := range opt {
o.apply(&opts)
}
return &Engine{
code: table,
opts: opts,
// g: req.NewParam("", nil),
}
}