From 693d73efac1d8a1b075ccd5839198924eaf9d05c Mon Sep 17 00:00:00 2001 From: ctfang <1992053348@qq.com> Date: Thu, 5 Nov 2026 22:17:52 +0800 Subject: [PATCH 1/2] feat(runtime): store Go context on the execution frame Child calls and includes inherit only the context saved on the frame. Restoring that stored value keeps a borrowed call deadline from hiding a later cancellation. Co-authored-by: Cursor --- runtime/context.go | 34 +++++++++++++++++++ runtime/execution_context_test.go | 56 +++++++++++++++++++++++++++++++ runtime/typed_call_bench_test.go | 6 ++-- runtime/vm.go | 8 +++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 runtime/execution_context_test.go diff --git a/runtime/context.go b/runtime/context.go index 10087e89..e428899f 100644 --- a/runtime/context.go +++ b/runtime/context.go @@ -47,6 +47,7 @@ type Context struct { pooled bool escaped bool strictTypes bool + goContext context.Context } func (c *Context) StrictTypes() bool { return c.strictTypes } @@ -191,6 +192,7 @@ func (c *Context) CreateContext(vars []data.Variable) data.Context { nc.pooled = true nc.escaped = false nc.strictTypes = c.strictTypes + nc.goContext = c.goContext nc.call = c.call nc.out = c.inheritOut() if nc.out != nil && nc.out.local { @@ -247,6 +249,7 @@ func (c *Context) ReleasePooled() { c.pooled = false c.vm = nil c.call = nil + c.goContext = nil c.out = nil c.callArgs = nil c.flatCallArgs = nil @@ -329,6 +332,7 @@ func (c *Context) CreateBaseContext() data.Context { call: call, out: out, strictTypes: c.strictTypes, + goContext: c.goContext, } } @@ -499,12 +503,42 @@ func (c *Context) GetVM() data.VM { } func (c *Context) GoContext() context.Context { + if c.goContext != nil { + return c.goContext + } if c.call != nil && c.call.deadline != nil { return c.call.deadline } return RequestContext() } +// SetGoContext binds execution values and cancellation to this frame only. +// Child frames inherit the immutable context; unrelated frames are unchanged. +func (c *Context) SetGoContext(ctx context.Context) { c.goContext = ctx } + +// RawGoContext is the value stored on this frame. It is nil when GoContext +// is only borrowing the call deadline, so callers can restore that distinction. +func (c *Context) RawGoContext() context.Context { return c.goContext } + +func storedGoContext(ctx data.Context) context.Context { + for i := 0; ctx != nil && i < 8; i++ { + if raw, ok := ctx.(interface{ RawGoContext() context.Context }); ok { + return raw.RawGoContext() + } + switch current := ctx.(type) { + case *data.ClassMethodContext: + ctx = current.Context + case *data.BoundContext: + ctx = current.Context + case *data.ClassValue: + ctx = current.Context + default: + return nil + } + } + return nil +} + // SetVM 替换当前 Context 所绑定的 VM func (c *Context) SetVM(vm data.VM) { if vm == nil { diff --git a/runtime/execution_context_test.go b/runtime/execution_context_test.go new file mode 100644 index 00000000..811efb4c --- /dev/null +++ b/runtime/execution_context_test.go @@ -0,0 +1,56 @@ +package runtime + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/php-any/origami/data" + "github.com/php-any/origami/parser" +) + +type executionContextKey struct{} +type executionContextProbe struct{} + +func (*executionContextProbe) GetName() string { return "execution_context_probe" } +func (*executionContextProbe) GetParams() []data.GetValue { return nil } +func (*executionContextProbe) GetVariables() []data.Variable { return nil } +func (*executionContextProbe) Call(ctx data.Context) (data.GetValue, data.Control) { + value, _ := ctx.GoContext().Value(executionContextKey{}).(string) + return data.NewStringValue(value), nil +} + +func TestExecutionContextInheritance(t *testing.T) { + vm := NewVM(parser.NewParser()) + vm.AddFunc(&executionContextProbe{}) + parent := vm.CreateContext(nil).(*Context) + goContext, cancel := context.WithCancel(context.WithValue(context.Background(), executionContextKey{}, "bound")) + defer cancel() + parent.SetGoContext(goContext) + for _, child := range []data.Context{parent.CreateContext(nil), parent.CreateBaseContext()} { + if child.GoContext().Value(executionContextKey{}) != "bound" { + t.Fatal("child did not inherit execution context") + } + data.ReleaseContext(child) + } + file := filepath.Join(t.TempDir(), "included.php") + if err := os.WriteFile(file, []byte(" 160 || unsafe.Sizeof(node.TokenFrom{}) > 56 { + // The stored context.Context is one interface, so the frame moves from the + // 160-byte size class to 176. That keeps a saved context distinct from the + // call deadline GoContext borrows when nothing has been stored. + if unsafe.Sizeof(Context{}) > 176 || unsafe.Sizeof(node.TokenFrom{}) > 56 { t.Fatal("type mode expanded every runtime frame or AST source location") } if unsafe.Sizeof(node.Parameter{}.Type) != 4 || unsafe.Sizeof(node.ClassProperty{}.Type) != 4 || unsafe.Sizeof(node.ClassMethod{}.Ret) != 4 || unsafe.Sizeof(node.FunctionStatement{}.Ret) != 4 || unsafe.Sizeof(node.InterfaceMethod{}.ReturnType) != 4 || unsafe.Sizeof(data.ParameterTODO{}.Type) != 4 { diff --git a/runtime/vm.go b/runtime/vm.go index f5a129fc..502f460a 100644 --- a/runtime/vm.go +++ b/runtime/vm.go @@ -1,6 +1,7 @@ package runtime import ( + "context" "fmt" "os" "sort" @@ -745,6 +746,13 @@ func inheritCallerScope(parent, ctx data.Context) data.Context { if parent == nil || ctx == nil { return ctx } + // Only an explicitly stored context is inherited. Copying GoContext() + // would pin its deadline fallback and hide a later request cancellation. + if stored := storedGoContext(parent); stored != nil { + if frame, ok := ctx.(interface{ SetGoContext(context.Context) }); ok { + frame.SetGoContext(stored) + } + } if bc := data.FindBoundContext(parent); bc != nil { ctx = &data.BoundContext{ Context: ctx, From 3671875693fa279930d10ee61740f5563a921982 Mon Sep 17 00:00:00 2001 From: ctfang <1992053348@qq.com> Date: Thu, 5 Nov 2026 22:17:52 +0800 Subject: [PATCH 2/2] feat(database): execute queries on a bound transaction Database\bind attaches the *sql.Tx returned by Database\Sql. The query uses that executor before the connection pool. Co-authored-by: Cursor --- std/database/bind.go | 99 +++++++++++++++++++++++++++++++++++++++++ std/database/db.go | 22 ++++++--- std/database/db_test.go | 23 ++++++++++ std/database/load.go | 1 + 4 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 std/database/bind.go create mode 100644 std/database/db_test.go diff --git a/std/database/bind.go b/std/database/bind.go new file mode 100644 index 00000000..ce545bb1 --- /dev/null +++ b/std/database/bind.go @@ -0,0 +1,99 @@ +package database + +import ( + "database/sql" + "errors" + + "github.com/php-any/origami/data" + "github.com/php-any/origami/node" + sqlpkg "github.com/php-any/origami/std/database/sql" + "github.com/php-any/origami/utils" +) + +// NewBindExecutorFunction 让 Database\DB 使用 Database\Sql 返回的 *sql.DB 或 *sql.Tx。 +func NewBindExecutorFunction() data.FuncStmt { return &BindExecutorFunction{} } + +type BindExecutorFunction struct{} + +func (f *BindExecutorFunction) Call(ctx data.Context) (data.GetValue, data.Control) { + builderValue, ok := ctx.GetIndexValue(0) + if !ok { + return nil, utils.NewThrow(errors.New("缺少查询器")) + } + targetValue, ok := ctx.GetIndexValue(1) + if !ok { + return nil, utils.NewThrow(errors.New("缺少数据库执行器")) + } + builder, ok := scriptObject(builderValue) + target, targetOK := scriptObject(targetValue) + if !ok || !targetOK { + return nil, utils.NewThrow(errors.New("查询器和执行器都必须是对象")) + } + state, ok := builderState(builder) + if !ok { + return nil, utils.NewThrow(errors.New("执行器只能绑定到 Database\\DB")) + } + switch class := target.Class.(type) { + case *sqlpkg.TxClass: + tx, _ := class.GetSource().(*sql.Tx) + if tx == nil { + return nil, utils.NewThrow(errors.New("事务尚未开始")) + } + state.exec = tx + case *sqlpkg.DBClass: + pool, _ := class.GetSource().(*sql.DB) + if pool == nil { + return nil, utils.NewThrow(errors.New("连接池尚未打开")) + } + state.exec = nil + state.conn = pool + default: + return nil, utils.NewThrow(errors.New("执行器必须是 Database\\Sql\\DB 或 Database\\Sql\\Tx")) + } + return builder, nil +} + +func (f *BindExecutorFunction) GetName() string { return "Database\\bind" } +func (f *BindExecutorFunction) GetParams() []data.GetValue { + return []data.GetValue{ + node.NewParameter(nil, "builder", 0, nil, data.NewBaseType("object")), + node.NewParameter(nil, "executor", 1, nil, data.NewBaseType("object")), + } +} +func (f *BindExecutorFunction) GetVariables() []data.Variable { + return []data.Variable{ + node.NewVariable(nil, "builder", 0, data.NewBaseType("object")), + node.NewVariable(nil, "executor", 1, data.NewBaseType("object")), + } +} + +func scriptObject(value data.GetValue) (*data.ClassValue, bool) { + switch current := value.(type) { + case *data.ClassValue: + return current, true + case *data.ThisValue: + if current == nil || current.ClassValue == nil { + return nil, false + } + return current.ClassValue, true + case *data.ZValValue: + if current.ZVal == nil { + return nil, false + } + return scriptObject(current.ZVal.ReadValue()) + default: + return nil, false + } +} + +func builderState(object *data.ClassValue) (*db, bool) { + class, ok := object.Class.(*DBClass) + if !ok || class == nil || class.queryMethod == nil { + return nil, false + } + query, ok := class.queryMethod.(*DbQueryMethod) + if !ok || query.source == nil { + return nil, false + } + return query.source, true +} diff --git a/std/database/db.go b/std/database/db.go index fdc1bede..6d9645e9 100644 --- a/std/database/db.go +++ b/std/database/db.go @@ -16,9 +16,16 @@ func newDB(genericMap map[string]data.Types) *db { } } +// sqlExecutor 是 *sql.DB 和 *sql.Tx 都具备的执行能力。 +type sqlExecutor interface { + Query(query string, args ...any) (*sql.Rows, error) + Exec(query string, args ...any) (sql.Result, error) +} + type db struct { - connName string // 连接名称 - conn *sql.DB // 数据库连接 + connName string // 连接名称 + conn *sql.DB // 数据库连接 + exec sqlExecutor // 显式执行器,事务时为 *sql.Tx // 查询条件 where string @@ -64,6 +71,7 @@ func (d *db) clone() *db { return &db{ connName: d.connName, conn: d.conn, + exec: d.exec, where: d.where, whereArgs: whereArgsCopy, tableName: d.tableName, @@ -77,8 +85,11 @@ func (d *db) clone() *db { } } -// getConnection 获取数据库连接 -func (d *db) getConnection() *sql.DB { +// getConnection 返回这次查询使用的执行器。显式绑定的事务优先于连接池。 +func (d *db) getConnection() sqlExecutor { + if d.exec != nil { + return d.exec + } if d.conn != nil { return d.conn } @@ -103,7 +114,8 @@ func (d *db) getConnection() *sql.DB { // setConnectionName 设置连接名称 func (d *db) setConnectionName(name string) { d.connName = name - d.conn = nil // 重置连接,下次调用时重新获取 + d.conn = nil + d.exec = nil } // setTableName 设置表名 diff --git a/std/database/db_test.go b/std/database/db_test.go new file mode 100644 index 00000000..732c1be4 --- /dev/null +++ b/std/database/db_test.go @@ -0,0 +1,23 @@ +package database + +import ( + "database/sql" + "testing" +) + +func TestGetConnectionPrefersExplicitExecutor(t *testing.T) { + pool := &sql.DB{} + tx := &sql.Tx{} + query := &db{conn: pool, exec: tx} + if query.getConnection() != tx { + t.Fatal("explicit transaction was not used") + } + query.exec = nil + if query.getConnection() != pool { + t.Fatal("pool was not used") + } + query.setConnectionName("other") + if query.exec != nil || query.conn != nil { + t.Fatal("connection name change kept the previous executor") + } +} diff --git a/std/database/load.go b/std/database/load.go index fa2c2b34..dcc9f259 100644 --- a/std/database/load.go +++ b/std/database/load.go @@ -25,6 +25,7 @@ func Load(vm data.VM) { // 注册数据库连接管理函数到脚本域 for _, fun := range []data.FuncStmt{ + NewBindExecutorFunction(), NewRegisterConnectionFunction(), NewRegisterDefaultConnectionFunction(), NewGetConnectionFunction(),