Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
921586f
fix(replace): apply parent-side foreign key actions during REPLACE
ck89119 Jun 23, 2026
33db07d
Merge branch 'main' into issue-24951-main
mergify[bot] Jun 23, 2026
a4af0e3
fix(replace): address review on parent-side FK handling
ck89119 Jun 23, 2026
c43253d
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 11, 2026
34f6a99
fix(plan): fail closed for unsafe replace FK actions
ck89119 Jul 11, 2026
4b87ef0
fix(plan): restrict replace for set default FKs
ck89119 Jul 12, 2026
3bf7617
Merge branch 'main' into issue-24951-main
XuPeng-SH Jul 12, 2026
c910bce
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 13, 2026
33aec1f
Merge branch 'main' into issue-24951-main
ck89119 Jul 13, 2026
feeb9a0
fix(plan): resolve replace FK actions from conflicts
ck89119 Jul 14, 2026
410e787
fix(plan): preserve unique prefix semantics in replace
ck89119 Jul 14, 2026
8da66fd
Merge branch 'main' into issue-24951-main
XuPeng-SH Jul 14, 2026
396b96a
fix(plan): materialize omitted replace conflict defaults
ck89119 Jul 15, 2026
054bb35
fix(plan): format temporal replace conflict defaults
ck89119 Jul 15, 2026
3e49c0d
fix(replace): serialize parent foreign key actions
ck89119 Jul 15, 2026
ce3795b
fix(plan): preserve lock mode in deep copy
ck89119 Jul 15, 2026
aad5a51
fix(plan): preserve lock target positions in deep copy
ck89119 Jul 15, 2026
17a826b
fix(plan): lock foreign key parent keys before validation
ck89119 Jul 17, 2026
9f87499
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 17, 2026
ea06887
fix(plan): support legacy foreign key parent locks
ck89119 Jul 17, 2026
50004c4
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 17, 2026
c2abe7b
fix(plan): preserve replace checks and shared table locks
ck89119 Jul 17, 2026
a624aab
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 18, 2026
8890557
fix(plan): align replace locks with referenced unique keys
ck89119 Jul 18, 2026
f758cbd
fix(plan): order foreign key prerequisite locks
ck89119 Jul 18, 2026
82d0410
fix(replace): address foreign key review feedback
ck89119 Jul 19, 2026
e0c8afb
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 19, 2026
f1b0efc
fix: address latest replace foreign key reviews
ck89119 Jul 20, 2026
d508df7
fix: support dynamic replace foreign key actions
ck89119 Jul 20, 2026
ddbb417
Merge remote-tracking branch 'mo/main' into issue-24951-main
ck89119 Jul 20, 2026
38c39f6
fix self-referencing replace cascade
ck89119 Jul 20, 2026
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
18 changes: 15 additions & 3 deletions pkg/frontend/computation_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,12 @@ func initExecuteStmtParam(execCtx *ExecCtx, ses *Session, cwft *TxnComputationWr
}
}

// rebuild plan when schema changed
if change {
// FK-sensitive plans also depend on the current foreign_key_checks session
// value, which does not invalidate prepared statements. Rebuild them for
// every EXECUTE so both enabled->disabled and disabled->enabled transitions
// observe the current setting.
fkSensitive := shouldRebuildPreparePlan(false, preparePlan.Plan)
if change || fkSensitive {
originPrepareStmt := &tree.PrepareStmt{
Name: tree.Identifier(prepareStmt.Name),
Stmt: prepareStmt.PrepareStmt,
Expand All @@ -568,7 +572,7 @@ func initExecuteStmtParam(execCtx *ExecCtx, ses *Session, cwft *TxnComputationWr
// query); recompiling would fail with ErrCantCompileForPrepare on every
// execution, so leave it to the regular compile path (isPrepare=false).
// See: https://github.com/matrixorigin/matrixone/issues/25614
if change && prepareStmt.compile != nil {
if (change || fkSensitive) && prepareStmt.compile != nil {
prepareStmt.compile.FreeOperator()
prepareStmt.compile.SetIsPrepare(false)
prepareStmt.compile.Release()
Expand Down Expand Up @@ -637,6 +641,14 @@ func shouldCachePrepareCompile(p *plan.Plan) bool {
return !query.GetHasForeignKeyAction()
}

func shouldRebuildPreparePlan(schemaChanged bool, p *plan.Plan) bool {
if schemaChanged || p == nil {
return schemaChanged
}
query := p.GetQuery()
return query != nil && query.GetHasForeignKeyAction()
}

func createCompile(
execCtx *ExecCtx,
ses FeSession,
Expand Down
3 changes: 3 additions & 0 deletions pkg/frontend/mysql_cmd_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4300,6 +4300,9 @@ func checkNodeCanCache(p *plan2.Plan) bool {
return true
}
if q, ok := p.Plan.(*plan2.Plan_Query); ok {
if q.Query.GetHasForeignKeyAction() {
return false
}
for _, node := range q.Query.Nodes {
if node.NotCacheable {
return false
Expand Down
9 changes: 9 additions & 0 deletions pkg/frontend/prepared_fk_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,13 @@ func TestShouldCachePrepareCompileForeignKeyActions(t *testing.T) {

require.False(t, shouldCachePrepareCompile(makePlan(plan.Query_UPDATE, true)))
require.False(t, shouldCachePrepareCompile(makePlan(plan.Query_DELETE, true)))
require.False(t, shouldCachePrepareCompile(makePlan(plan.Query_INSERT, true)))

require.True(t, checkNodeCanCache(makePlan(plan.Query_INSERT, false)))
require.False(t, checkNodeCanCache(makePlan(plan.Query_INSERT, true)))

require.False(t, shouldRebuildPreparePlan(false, nil))
require.False(t, shouldRebuildPreparePlan(false, makePlan(plan.Query_INSERT, false)))
require.True(t, shouldRebuildPreparePlan(false, makePlan(plan.Query_INSERT, true)))
require.True(t, shouldRebuildPreparePlan(true, makePlan(plan.Query_INSERT, false)))
}
17 changes: 15 additions & 2 deletions pkg/sql/colexec/lockop/lock_op.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func performLock(
target.primaryColumnType,
target.partitionColumnIndexInBatch,
DefaultLockOptions(lockOp.ctr.parker).
WithLockMode(lock.LockMode_Exclusive).
WithLockMode(target.mode).
WithFetchLockRowsFunc(lockOp.ctr.fetchers[idx]).
WithMaxBytesPerLock(int(proc.GetLockService().GetConfig().MaxLockRowCount)).
WithFilterRows(target.filter, filterCols).
Expand Down Expand Up @@ -313,6 +313,17 @@ func LockTable(
tableID uint64,
pkType types.Type,
changeDef bool) error {
return LockTableWithMode(eng, proc, tableID, pkType, lock.LockMode_Exclusive, changeDef)
}

// LockTableWithMode locks all rows in a table with the specified lock mode.
func LockTableWithMode(
eng engine.Engine,
proc *process.Process,
tableID uint64,
pkType types.Type,
mode lock.LockMode,
changeDef bool) error {
txnOp := proc.GetTxnOperator()
if !txnOp.Txn().IsPessimistic() {
return nil
Expand All @@ -336,6 +347,7 @@ func LockTable(
}()

opts := DefaultLockOptions(parker).
WithLockMode(mode).
WithLockTable(true, changeDef).
WithFetchLockRowsFunc(GetFetchRowsFunc(pkType))
_, defChanged, refreshTS, err := doLock(
Expand Down Expand Up @@ -1540,7 +1552,8 @@ func lockTalbeIfLockCountIsZero(
if !target.lockTableAtTheEnd {
continue
}
err := LockTable(lockOp.engine, proc, target.tableID, target.primaryColumnType, false)
err := LockTableWithMode(
lockOp.engine, proc, target.tableID, target.primaryColumnType, target.mode, false)
if err != nil {
return err
}
Expand Down
15 changes: 13 additions & 2 deletions pkg/sql/colexec/lockop/lock_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,15 +1087,26 @@ func TestCallLockOpLocksTableAtEOFWhenNoRowsProduced(t *testing.T) {
IsFirst: false,
IsLast: false,
}
arg.AddLockTarget(tableID, nil, 0, pkType, -1, -1, nil, true)
arg.LockTable(tableID, false)
arg.AddLockTargetWithMode(
tableID, nil, lock.LockMode_Shared, 0, pkType, -1, -1, nil, true)
arg.LockTableWithMode(tableID, lock.LockMode_Shared, false)
resetChildren(arg, nil)
defer arg.Free(proc, false, nil)

require.NoError(t, arg.Prepare(proc))
_, err := vm.Exec(arg, proc)
require.NoError(t, err)
require.True(t, proc.GetTxnOperator().HasLockTable(tableID))

sharedTxn, err := proc.Base.TxnClient.New(proc.Ctx, timestamp.Timestamp{})
require.NoError(t, err)
defer func() { require.NoError(t, sharedTxn.Rollback(proc.Ctx)) }()
sharedProc := process.NewTopProcess(proc.Ctx, mpool.MustNewZero(), proc.Base.TxnClient,
sharedTxn, nil, proc.GetLockService(), nil, nil, nil, nil, nil)
require.NoError(t, LockTableWithMode(
nil, sharedProc, tableID, pkType, lock.LockMode_Shared, false))
require.NoError(t, LockTable(nil, proc, tableID+1, pkType, false))
require.True(t, proc.GetTxnOperator().HasLockTable(tableID+1))
},
)
}
Expand Down
46 changes: 38 additions & 8 deletions pkg/sql/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,21 @@ func (c *Compile) runOnce() (err error) {
c.proc.Base.StageCache.Clear()
}()

// Pre-check: REPLACE parent→child FK RESTRICT constraints must be
// verified before the REPLACE execution modifies any rows.
// REPLACE parent checks and actions run before the main pipeline.
query := c.pn.GetQuery()
if query != nil && query.StmtType == plan.Query_INSERT && len(query.GetDetectSqls()) != 0 {
if err = validateReplaceParentTxnMode(
c.proc.Ctx, query, c.proc.GetTxnOperator().Txn().IsPessimistic()); err != nil {
return err
}
for _, sql := range query.DetectSqls {
if strings.HasPrefix(sql, "REPLACE_PARENT_CHK:") {
if strings.HasPrefix(sql, "REPLACE_PARENT_PLAN:") {
continue
} else if strings.HasPrefix(sql, "REPLACE_PARENT_LOCK:") {
if err = c.runSql(strings.TrimPrefix(sql, "REPLACE_PARENT_LOCK:")); err != nil {
return err
}
} else if strings.HasPrefix(sql, "REPLACE_PARENT_CHK:") {
if err = runDetectSql(c, strings.TrimPrefix(sql, "REPLACE_PARENT_CHK:")); err != nil {
// Only translate the "check returned false" signal into the
// parent-row-referenced error; pass through real execution
Expand All @@ -541,6 +550,10 @@ func (c *Compile) runOnce() (err error) {
}
return err
}
} else if strings.HasPrefix(sql, "REPLACE_PARENT_ACTION:") {
if err = c.runSql(strings.TrimPrefix(sql, "REPLACE_PARENT_ACTION:")); err != nil {
return err
}
}
}
}
Expand Down Expand Up @@ -634,12 +647,14 @@ func (c *Compile) runOnce() (err error) {
query = c.pn.GetQuery()
if query != nil && (query.StmtType == plan.Query_INSERT ||
query.StmtType == plan.Query_UPDATE) && len(query.GetDetectSqls()) != 0 {
// Filter out pre-check SQLs (already executed before the main operation).
// The modern INSERT path enforces child→parent existence in-plan now, so the
// remaining DetectSqls are self-referencing FK checks (plain 1452 message).
// Filter out REPLACE parent-side checks and actions already executed before
// the main operation.
var postCheckSqls []string
for _, sql := range query.DetectSqls {
if strings.HasPrefix(sql, "REPLACE_PARENT_CHK:") {
if strings.HasPrefix(sql, "REPLACE_PARENT_LOCK:") ||
strings.HasPrefix(sql, "REPLACE_PARENT_PLAN:") ||
strings.HasPrefix(sql, "REPLACE_PARENT_CHK:") ||
strings.HasPrefix(sql, "REPLACE_PARENT_ACTION:") {
continue
}
postCheckSqls = append(postCheckSqls, sql)
Expand All @@ -656,6 +671,20 @@ func (c *Compile) runOnce() (err error) {
return err
}

func validateReplaceParentTxnMode(ctx context.Context, query *plan.Query, pessimistic bool) error {
if pessimistic || query == nil {
return nil
}
for _, sql := range query.DetectSqls {
if strings.HasPrefix(sql, "REPLACE_PARENT_LOCK:") ||
strings.HasPrefix(sql, "REPLACE_PARENT_PLAN:") {
return moerr.NewNotSupported(ctx,
"REPLACE on a referenced parent table in optimistic transaction mode")
}
}
return nil
}

// add log to check if background sql return NeedRetry error when origin sql execute successfully
func (c *Compile) debugLogFor19288(err error, bsql string) {
if c.isRetryErr(err) {
Expand Down Expand Up @@ -807,11 +836,12 @@ func (c *Compile) lockTable() error {
for _, tableID := range tableIDs {
tbl := c.lockTables[tableID]
typ := plan2.MakeTypeByPlan2Type(tbl.PrimaryColTyp)
if err := lockop.LockTable(
if err := lockop.LockTableWithMode(
c.e,
c.proc,
tbl.TableId,
typ,
tbl.Mode,
false); err != nil {
return err
}
Expand Down
49 changes: 47 additions & 2 deletions pkg/sql/compile/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ import (
"github.com/matrixorigin/matrixone/pkg/container/vector"
"github.com/matrixorigin/matrixone/pkg/defines"
"github.com/matrixorigin/matrixone/pkg/lockservice"
lockpb "github.com/matrixorigin/matrixone/pkg/pb/lock"
"github.com/matrixorigin/matrixone/pkg/pb/plan"
"github.com/matrixorigin/matrixone/pkg/pb/timestamp"
"github.com/matrixorigin/matrixone/pkg/perfcounter"
"github.com/matrixorigin/matrixone/pkg/sql/colexec/lockop"
"github.com/matrixorigin/matrixone/pkg/txn/client"
"github.com/matrixorigin/matrixone/pkg/txn/rpc"

Expand All @@ -45,7 +48,6 @@ import (
"github.com/matrixorigin/matrixone/pkg/common/buffer"
"github.com/matrixorigin/matrixone/pkg/container/batch"
mock_frontend "github.com/matrixorigin/matrixone/pkg/frontend/test"
"github.com/matrixorigin/matrixone/pkg/pb/plan"
"github.com/matrixorigin/matrixone/pkg/pb/txn"
"github.com/matrixorigin/matrixone/pkg/sql/colexec"
"github.com/matrixorigin/matrixone/pkg/sql/colexec/group"
Expand Down Expand Up @@ -214,6 +216,40 @@ func TestShouldPrePipelineLockTable(t *testing.T) {
require.False(t, target.LockTableAtTheEnd)
}

func TestConstructLockOpPreservesSharedTableMode(t *testing.T) {
for _, lockTable := range []bool{false, true} {
t.Run(fmt.Sprintf("table=%t", lockTable), func(t *testing.T) {
node := &plan.Node{LockTargets: []*plan.LockTarget{{
TableId: 42, PrimaryColTyp: plan.Type{Id: int32(types.T_int64)},
Mode: lockpb.LockMode_Shared, LockTable: lockTable,
}}}

op, err := constructLockOp(node, nil)
require.NoError(t, err)
targets := op.CopyToPipelineTarget()
require.Len(t, targets, 1)
assert.Equal(t, lockTable, targets[0].LockTable)
assert.Equal(t, lockpb.LockMode_Shared, targets[0].Mode)
})
}
}

func TestValidateReplaceParentTxnMode(t *testing.T) {
ctx := context.Background()
query := &plan.Query{DetectSqls: []string{"REPLACE_PARENT_LOCK:select 1 for update"}}

require.NoError(t, validateReplaceParentTxnMode(ctx, query, true))
require.ErrorContains(t, validateReplaceParentTxnMode(ctx, query, false),
"optimistic transaction mode")
query.DetectSqls = []string{"REPLACE_PARENT_PLAN:"}
require.NoError(t, validateReplaceParentTxnMode(ctx, query, true))
require.ErrorContains(t, validateReplaceParentTxnMode(ctx, query, false),
"optimistic transaction mode")
require.NoError(t, validateReplaceParentTxnMode(ctx,
&plan.Query{DetectSqls: []string{"select true"}}, false))
require.NoError(t, validateReplaceParentTxnMode(ctx, nil, false))
}

func TestLockTableLocksAllPrePipelineTargets(t *testing.T) {
runtime.RunTest(
"",
Expand Down Expand Up @@ -259,14 +295,23 @@ func TestLockTableLocksAllPrePipelineTargets(t *testing.T) {
c := &Compile{
proc: proc,
lockTables: map[uint64]*plan.LockTarget{
10: {TableId: 10, PrimaryColTyp: plan.Type{Id: int32(types.T_int32)}},
10: {TableId: 10, PrimaryColTyp: plan.Type{Id: int32(types.T_int32)},
Mode: lockpb.LockMode_Shared},
11: {TableId: 11, PrimaryColTyp: plan.Type{Id: int32(types.T_int32)}},
},
}

require.NoError(t, c.lockTable())
require.True(t, txnOp.HasLockTable(10))
require.True(t, txnOp.HasLockTable(11))

sharedTxn, err := txnClient.New(ctx, timestamp.Timestamp{})
require.NoError(t, err)
defer func() { require.NoError(t, sharedTxn.Rollback(ctx)) }()
sharedProc := process.NewTopProcess(ctx, mpool.MustNewZero(), txnClient, sharedTxn,
nil, services[0], nil, nil, nil, nil, nil)
require.NoError(t, lockop.LockTableWithMode(nil, sharedProc, 10,
types.T_int32.ToType(), lockpb.LockMode_Shared, false))
},
nil,
)
Expand Down
4 changes: 2 additions & 2 deletions pkg/sql/compile/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,11 +788,11 @@ func constructLockOp(node *plan.Node, eng engine.Engine) (*lockop.LockOp, error)
partitionColPos = target.PartitionColIdxInBat
}
typ := plan2.MakeTypeByPlan2Type(target.PrimaryColTyp)
arg.AddLockTarget(target.GetTableId(), target.GetObjRef(), target.GetPrimaryColIdxInBat(), typ, partitionColPos, target.GetRefreshTsIdxInBat(), target.GetLockRows(), target.GetLockTableAtTheEnd())
arg.AddLockTargetWithMode(target.GetTableId(), target.GetObjRef(), target.GetMode(), target.GetPrimaryColIdxInBat(), typ, partitionColPos, target.GetRefreshTsIdxInBat(), target.GetLockRows(), target.GetLockTableAtTheEnd())
}
for _, target := range node.LockTargets {
if target.LockTable {
arg.LockTable(target.TableId, false)
arg.LockTableWithMode(target.TableId, target.Mode, false)
}
}
return arg, nil
Expand Down
4 changes: 2 additions & 2 deletions pkg/sql/compile/remoterun.go
Original file line number Diff line number Diff line change
Expand Up @@ -943,11 +943,11 @@ func convertToVmOperator(opr *pipeline.Instruction, ctx *scopeContext, eng engin
lockArg := lockop.NewArgumentByEngine(eng)
for _, target := range t.Targets {
typ := plan2.MakeTypeByPlan2Type(target.PrimaryColTyp)
lockArg.AddLockTarget(target.GetTableId(), target.GetObjRef(), target.GetPrimaryColIdxInBat(), typ, target.PartitionColIdxInBat, target.GetRefreshTsIdxInBat(), target.GetLockRows(), target.GetLockTableAtTheEnd())
lockArg.AddLockTargetWithMode(target.GetTableId(), target.GetObjRef(), target.GetMode(), target.GetPrimaryColIdxInBat(), typ, target.PartitionColIdxInBat, target.GetRefreshTsIdxInBat(), target.GetLockRows(), target.GetLockTableAtTheEnd())
}
for _, target := range t.Targets {
if target.LockTable {
lockArg.LockTable(target.TableId, target.ChangeDef)
lockArg.LockTableWithMode(target.TableId, target.Mode, target.ChangeDef)
}
}
op = lockArg
Expand Down
32 changes: 32 additions & 0 deletions pkg/sql/compile/remoterun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/matrixorigin/matrixone/pkg/container/vector"
"github.com/matrixorigin/matrixone/pkg/defines"
mock_frontend "github.com/matrixorigin/matrixone/pkg/frontend/test"
lockpb "github.com/matrixorigin/matrixone/pkg/pb/lock"
"github.com/matrixorigin/matrixone/pkg/pb/pipeline"
planpb "github.com/matrixorigin/matrixone/pkg/pb/plan"
"github.com/matrixorigin/matrixone/pkg/pb/txn"
Expand Down Expand Up @@ -359,6 +360,37 @@ func TestRemoteRunOperatorCodecRoundTrip(t *testing.T) {
require.IsType(t, &intersectall.IntersectAll{}, restored)
require.Equal(t, vm.IntersectAll, restored.OpType())
})

t.Run("SharedTableLock", func(t *testing.T) {
original := lockop.NewArgumentByEngine(nil)
original.AddLockTargetWithMode(42, nil, lockpb.LockMode_Shared, 0,
types.T_int64.ToType(), -1, -1, nil, false)
original.LockTableWithMode(42, lockpb.LockMode_Shared, false)

restored := roundTrip(t, original)
defer restored.Release()
restoredLock, ok := restored.(*lockop.LockOp)
require.True(t, ok)
targets := restoredLock.CopyToPipelineTarget()
require.Len(t, targets, 1)
require.True(t, targets[0].LockTable)
require.Equal(t, lockpb.LockMode_Shared, targets[0].Mode)
})

t.Run("SharedRowLock", func(t *testing.T) {
original := lockop.NewArgumentByEngine(nil)
original.AddLockTargetWithMode(43, nil, lockpb.LockMode_Shared, 0,
types.T_int64.ToType(), -1, -1, nil, false)

restored := roundTrip(t, original)
defer restored.Release()
restoredLock, ok := restored.(*lockop.LockOp)
require.True(t, ok)
targets := restoredLock.CopyToPipelineTarget()
require.Len(t, targets, 1)
require.False(t, targets[0].LockTable)
require.Equal(t, lockpb.LockMode_Shared, targets[0].Mode)
})
}

func TestExternalScanParquetRowGroupShardsRoundtrip(t *testing.T) {
Expand Down
Loading
Loading