Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions runtime/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type Context struct {
pooled bool
escaped bool
strictTypes bool
goContext context.Context
}

func (c *Context) StrictTypes() bool { return c.strictTypes }
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -329,6 +332,7 @@ func (c *Context) CreateBaseContext() data.Context {
call: call,
out: out,
strictTypes: c.strictTypes,
goContext: c.goContext,
}
}

Expand Down Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions runtime/execution_context_test.go
Original file line number Diff line number Diff line change
@@ -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("<?php return execution_context_probe();"), 0600); err != nil {
t.Fatal(err)
}
value, ctl := vm.LoadInCallerContext(parent, file)
if returned, ok := ctl.(data.ReturnControl); ok {
value, ctl = returned.ReturnValue(), nil
}
if ctl != nil || value.(data.AsString).AsString() != "bound" {
t.Fatalf("include lost context: %v %v", value, ctl)
}
cancel()
if parent.CreateContext(nil).GoContext().Err() != context.Canceled {
t.Fatal("child lost cancellation")
}
parent.ReleasePooled()
if vm.CreateContext(nil).GoContext().Value(executionContextKey{}) != nil {
t.Fatal("pooled frame retained execution values")
}
}
6 changes: 4 additions & 2 deletions runtime/typed_call_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ func TestTypeModeStorageSize(t *testing.T) {
}
t.Logf("Context=%d TokenFrom=%d", unsafe.Sizeof(Context{}), unsafe.Sizeof(node.TokenFrom{}))
// One pointer owns reusable buckets; type mode still uses existing padding.
// The 160-byte frame stays in the same allocation size class as 152 bytes.
if unsafe.Sizeof(Context{}) > 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 {
Expand Down
8 changes: 8 additions & 0 deletions runtime/vm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runtime

import (
"context"
"fmt"
"os"
"sort"
Expand Down Expand Up @@ -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,
Expand Down
99 changes: 99 additions & 0 deletions std/database/bind.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 17 additions & 5 deletions std/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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 设置表名
Expand Down
23 changes: 23 additions & 0 deletions std/database/db_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
1 change: 1 addition & 0 deletions std/database/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func Load(vm data.VM) {

// 注册数据库连接管理函数到脚本域
for _, fun := range []data.FuncStmt{
NewBindExecutorFunction(),
NewRegisterConnectionFunction(),
NewRegisterDefaultConnectionFunction(),
NewGetConnectionFunction(),
Expand Down
Loading