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
19 changes: 19 additions & 0 deletions pkg/logservice/gossip_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,25 @@ func TestGossipFixtureStartupConfigurationFailure(t *testing.T) {
}
}

func TestGossipFixtureConfigPortRange(t *testing.T) {
first, limit, err := testPortRange()
require.NoError(t, err)
rt := runtime.DefaultRuntime()
configs, err := makeGossipFixtureConfigs(t, rt, 2)
defer func() { require.NoError(t, closeGossipFixture(rt, configs, nil)) }()
require.NoError(t, err)
require.Len(t, configs, 2)
seen := make(map[int]struct{})
for _, cfg := range configs {
for _, port := range []int{cfg.GossipPort, cfg.RaftPort, cfg.LogServicePort} {
require.GreaterOrEqual(t, port, first)
require.Less(t, port, limit)
require.NotContains(t, seen, port)
seen[port] = struct{}{}
}
}
}

func TestGossipFixtureConfigAllocationFailure(t *testing.T) {
for _, tc := range []struct {
name string
Expand Down
1 change: 1 addition & 0 deletions pkg/sql/plan/bind_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func canDeleteRewriteToTruncate(ctx CompilerContext, dmlCtx *DMLContext) (bool,

for i, tableDef := range dmlCtx.tableDefs {
if enabled && len(tableDef.RefChildTbls) > 0 ||
tableDef.IsTemporary ||
tableDef.ViewSql != nil ||
(dmlCtx.isClusterTable[i] && accountId != catalog.System_Account) ||
dmlCtx.objRefs[i].PubInfo != nil ||
Expand Down
8 changes: 6 additions & 2 deletions pkg/sql/plan/build_dml_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,9 @@ func buildDeletePlans(ctx CompilerContext, builder *QueryBuilder, bindCtx *BindC
// Refer to this PR:https://github.com/matrixorigin/matrixone/pull/12093
// we have build SK using UK code path. So we might see UK in function signature even thought it could be for
// both UK and SK. To handle SK case, we will have flags to indicate if it's UK or SK.
canTruncate := delCtx.isUnrestrictedDelete
// Temporary DELETE is transactional row DML. A physical replacement would
// enter the persistent-table lifecycle using its private physical name.
canTruncate := delCtx.isUnrestrictedDelete && !delCtx.tableDef.IsTemporary

enabled, err := IsForeignKeyChecksEnabled(ctx)
if err != nil {
Expand Down Expand Up @@ -7061,7 +7063,9 @@ func buildDeleteIndexPlans(ctx CompilerContext, builder *QueryBuilder, bindCtx *
// both UK and SK. To handle SK case, we will have flags to indicate if it's UK or SK.
hasUniqueKey := haveUniqueKey(delCtx.tableDef)
hasSecondaryKey := haveSecondaryKey(delCtx.tableDef)
canTruncate := delCtx.isUnrestrictedDelete
// Keep temporary index maintenance on the same row-delete path as its base
// table; skipping it would leave stale index entries after DELETE.
canTruncate := delCtx.isUnrestrictedDelete && !delCtx.tableDef.IsTemporary

accountId, err := ctx.GetAccountId()
if err != nil {
Expand Down
62 changes: 62 additions & 0 deletions pkg/sql/plan/temporary_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2026 Matrix Origin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package plan

import (
"testing"

planpb "github.com/matrixorigin/matrixone/pkg/pb/plan"
"github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect/mysql"
"github.com/matrixorigin/matrixone/pkg/sql/parsers/tree"
"github.com/stretchr/testify/require"
)

func TestTemporaryDeleteDoesNotRewriteToTruncate(t *testing.T) {
for _, temporary := range []bool{false, true} {
name := "permanent"
if temporary {
name = "temporary"
}
t.Run(name, func(t *testing.T) {
mock := NewMockOptimizer(true, newPlanTestProcess(t))
table := mock.ctxt.tables["nation"]
table.IsTemporary = temporary
ctx := &DMLContext{
tableDefs: []*TableDef{table},
objRefs: []*ObjectRef{{SchemaName: "tpch", ObjName: "nation"}},
isClusterTable: []bool{false},
}
allowed, err := canDeleteRewriteToTruncate(&mock.ctxt, ctx)
require.NoError(t, err)
require.Equal(t, !temporary, allowed)

// Exercise the legacy planner too: it has a separate truncate
// eligibility check and must not reintroduce a physical replacement.
stmt, err := mysql.ParseOne(t.Context(), "delete from tpch.nation", 1)
require.NoError(t, err)
defer stmt.Free()
p, err := buildDelete(stmt.(*tree.Delete), &mock.ctxt, false)
require.NoError(t, err)
found := false
for _, node := range p.GetQuery().Nodes {
if node.NodeType == planpb.Node_DELETE && node.DeleteCtx.TableDef.TblId == table.TblId {
found = true
require.Equal(t, !temporary, node.DeleteCtx.CanTruncate)
}
}
require.True(t, found, "expected an origin-table DELETE node")
})
}
}
112 changes: 112 additions & 0 deletions pkg/tests/issues/issue_30089_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2026 Matrix Origin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package issues

import (
"context"
"database/sql"
"fmt"
"testing"
"time"

"github.com/matrixorigin/matrixone/pkg/embed"
"github.com/stretchr/testify/require"
)

func TestIssue30089TemporaryDeleteAvoidsPersistentLifecycle(t *testing.T) {
runAuthenticatedClusterTest(t, func(cluster embed.Cluster) {
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
defer cancel()
cn0, err := cluster.GetCNService(0)
require.NoError(t, err)
cn1, err := cluster.GetCNService(1)
require.NoError(t, err)
db0, err := sql.Open("mysql", issue27487DSN(cn0.GetServiceConfig().CN.Frontend.Port))
require.NoError(t, err)
defer db0.Close()
db1, err := sql.Open("mysql", issue27487DSN(cn1.GetServiceConfig().CN.Frontend.Port))
require.NoError(t, err)
defer db1.Close()
const database = "issue30089_temp_delete"
execSQLRequire(t, ctx, db0, "create database "+database)
defer func() {
cleanup, stop := context.WithTimeout(context.Background(), 20*time.Second)
defer stop()
_, err := db0.ExecContext(cleanup, "drop database if exists "+database)
require.NoError(t, err)
}()
for _, indexed := range []bool{false, true} {
t.Run(fmt.Sprintf("indexed_%t", indexed), func(t *testing.T) {
conn, err := db0.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
exec := func(query string) {
t.Helper()
_, err := conn.ExecContext(ctx, query)
require.NoError(t, err, query)
}
exec("use " + database)
table := fmt.Sprintf("t_%t", indexed)
exec("create table " + table + "(id int primary key, v int)")
exec("insert into " + table + " values(99,99)")
definition := "(id int primary key, v int"
if indexed {
definition += ", unique key uk(v)"
}
exec("create temporary table " + table + definition + ")")
defer func() {
cleanup, stop := context.WithTimeout(context.Background(), 20*time.Second)
defer stop()
_, _ = conn.ExecContext(cleanup, "rollback")
_, err := conn.ExecContext(cleanup, "drop temporary table if exists "+table)
require.NoError(t, err)
}()
exec("insert into " + table + " values(2,20)")
exec("begin")
exec("update " + table + " set id=4, v=40")

// A real shared owner on the other CN is a deterministic barrier:
// the old physical TRUNCATE rewrite tries to promote to G X and
// fails because UPDATE has already retained table locks.
holder, err := db1.BeginTx(ctx, nil)
require.NoError(t, err)
defer holder.Rollback()
var feature string
require.NoError(t, holder.QueryRowContext(ctx,
"select feature_code from mo_catalog.mo_feature_registry where feature_code='SNAPSHOT' for share").Scan(&feature))
require.Equal(t, "SNAPSHOT", feature)
deleteCtx, stop := context.WithTimeout(ctx, 10*time.Second)
_, err = conn.ExecContext(deleteCtx, "delete from "+table)
stop()
require.NoError(t, err)
exec("insert into " + table + " values(5,40)")
exec("rollback")
var id, value int
require.NoError(t, conn.QueryRowContext(ctx, "select id,v from "+table).Scan(&id, &value))
require.Equal(t, 2, id)
require.Equal(t, 20, value)
// Committed DELETE must remove the unique index entry too.
exec("delete from " + table)
exec("insert into " + table + " values(3,20)")
require.NoError(t, conn.QueryRowContext(ctx, "select id from "+table+" where v=20").Scan(&id))
require.Equal(t, 3, id)
require.NoError(t, holder.Rollback())
// The same-name permanent relation is not the delete target.
require.NoError(t, db1.QueryRowContext(ctx, "select id from "+database+"."+table).Scan(&id))
require.Equal(t, 99, id)
})
}
})
}
Loading