From fd049f83bd5933b5748238d08c3026af33482772 Mon Sep 17 00:00:00 2001 From: gmhdbjd Date: Thu, 18 Jun 2026 08:50:34 +0800 Subject: [PATCH] checker(dm): improve same table precheck error --- dm/checker/check_test.go | 93 ++++++++++++++++++++++++++++++++++++++++ dm/checker/checker.go | 59 +++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/dm/checker/check_test.go b/dm/checker/check_test.go index 733780ad0f..ac63e1c92c 100644 --- a/dm/checker/check_test.go +++ b/dm/checker/check_test.go @@ -26,6 +26,7 @@ import ( "github.com/pingcap/tiflow/dm/ctl/common" "github.com/pingcap/tiflow/dm/pkg/conn" "github.com/pingcap/tiflow/dm/pkg/cputil" + "github.com/pingcap/tiflow/dm/unit" "github.com/stretchr/testify/require" ) @@ -794,6 +795,98 @@ func TestSameTargetTableDetection(t *testing.T) { require.ErrorContains(t, err, "same table name in case-insensitive") } +func TestSameTargetTableDetectionBeforeRouteDupMatch(t *testing.T) { + cfgs := []*config.SubTaskConfig{ + { + RouteRules: []*router.TableRule{ + { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "T", + TargetTable: "T", + }, { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "t", + TargetTable: "t", + }, + }, + Mode: config.ModeAll, + IgnoreCheckingItems: ignoreExcept(map[string]struct{}{config.TableSchemaChecking: {}}), + }, + } + cfgsWithSameTarget := []*config.SubTaskConfig{ + { + RouteRules: []*router.TableRule{ + { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "T", + TargetTable: "T", + }, { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "t", + TargetTable: "T", + }, + }, + Mode: config.ModeAll, + IgnoreCheckingItems: ignoreExcept(map[string]struct{}{config.TableSchemaChecking: {}}), + }, + } + + require.NoError(t, sameTargetTableNameDetectionForRouteRules(false, []*router.TableRule{ + { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "a", + TargetTable: "T", + }, { + SchemaPattern: "test", + TargetSchema: "test", + TablePattern: "b", + TargetTable: "t", + }, + })) + require.NoError(t, sameTargetTableNameDetectionForRouteRules(true, cfgs[0].RouteRules)) + + msg, err := CheckSyncConfig(context.Background(), cfgs, common.DefaultErrorCnt, common.DefaultWarnCnt) + require.ErrorContains(t, err, "fail to initialize checker") + require.ErrorContains(t, err, "same table name in case-insensitive") + require.ErrorContains(t, err, "same target table `test`.`T` vs `test`.`t`") + require.NotContains(t, err.Error(), "matches more than one rule") + require.Len(t, msg, 0) + + _, err = RunCheckOnConfigs(context.Background(), cfgs, false, 100, 100) + require.ErrorContains(t, err, "fail to initialize checker") + require.ErrorContains(t, err, "same table name in case-insensitive") + require.ErrorContains(t, err, "same target table `test`.`T` vs `test`.`t`") + require.NotContains(t, err.Error(), "matches more than one rule") + + processErr := unit.NewProcessError(err) + require.Contains(t, processErr.Message, "same table name in case-insensitive") + require.Contains(t, processErr.Message, "same target table `test`.`T` vs `test`.`t`") + require.Empty(t, processErr.RawCause) + + msg, err = CheckSyncConfig(context.Background(), cfgsWithSameTarget, common.DefaultErrorCnt, common.DefaultWarnCnt) + require.ErrorContains(t, err, "fail to initialize checker") + require.ErrorContains(t, err, "same table name in case-insensitive") + require.ErrorContains(t, err, "same target table `test`.`T` vs `test`.`T`") + require.NotContains(t, err.Error(), "matches more than one rule") + require.Len(t, msg, 0) + + _, err = RunCheckOnConfigs(context.Background(), cfgsWithSameTarget, false, 100, 100) + require.ErrorContains(t, err, "fail to initialize checker") + require.ErrorContains(t, err, "same table name in case-insensitive") + require.ErrorContains(t, err, "same target table `test`.`T` vs `test`.`T`") + require.NotContains(t, err.Error(), "matches more than one rule") + + processErr = unit.NewProcessError(err) + require.Contains(t, processErr.Message, "same table name in case-insensitive") + require.Contains(t, processErr.Message, "same target table `test`.`T` vs `test`.`T`") + require.Empty(t, processErr.RawCause) +} + func TestMetaPositionChecking(t *testing.T) { cfgs := []*config.SubTaskConfig{ { diff --git a/dm/checker/checker.go b/dm/checker/checker.go index 69e6eb076a..93ac69add4 100644 --- a/dm/checker/checker.go +++ b/dm/checker/checker.go @@ -37,6 +37,7 @@ import ( "github.com/pingcap/tidb/pkg/util/dbutil" "github.com/pingcap/tidb/pkg/util/filter" regexprrouter "github.com/pingcap/tidb/pkg/util/regexpr-router" + router "github.com/pingcap/tidb/pkg/util/table-router" "github.com/pingcap/tiflow/dm/config" "github.com/pingcap/tiflow/dm/config/dbconfig" "github.com/pingcap/tiflow/dm/loader" @@ -566,6 +567,9 @@ func (c *Checker) fetchSourceTargetDB( return nil, nil, terror.ErrTaskCheckGenBAList.Delegate(err) } instance.baList = bAList + if err := sameTargetTableNameDetectionForRouteRules(instance.cfg.CaseSensitive, instance.cfg.RouteRules); err != nil { + return nil, nil, err + } r, err := regexprrouter.NewRegExprRouter(instance.cfg.CaseSensitive, instance.cfg.RouteRules) if err != nil { return nil, nil, terror.ErrTaskCheckGenTableRouter.Delegate(err) @@ -876,6 +880,61 @@ func sameTableNameDetection(tables map[filter.Table][]filter.Table) error { return nil } +func sameTargetTableNameDetectionForRouteRules(caseSensitive bool, rules []*router.TableRule) error { + if caseSensitive { + return nil + } + + // regexprrouter matches source patterns case-insensitively in this mode and + // reports "matches more than one rule" before the checker can build target + // table mappings. Detect the same-target-table conflict from the route + // rules first so the precheck reports the actionable table names. + sourceTableToTargetNameSets := make(map[string]map[string]string) + var messages []string + for _, rule := range rules { + if rule == nil || rule.TablePattern == "" || rule.TargetSchema == "" { + continue + } + + targetTable := rule.TargetTable + if targetTable == "" { + targetTable = rule.TablePattern + } + if targetTable == "" { + continue + } + + source := filter.Table{ + Schema: rule.SchemaPattern, + Name: rule.TablePattern, + } + sourceNameL := strings.ToLower(source.String()) + tableNameSets := sourceTableToTargetNameSets[sourceNameL] + if tableNameSets == nil { + tableNameSets = make(map[string]string) + sourceTableToTargetNameSets[sourceNameL] = tableNameSets + } + + target := filter.Table{ + Schema: rule.TargetSchema, + Name: targetTable, + } + name := target.String() + nameL := strings.ToLower(name) + if nameO, ok := tableNameSets[nameL]; !ok { + tableNameSets[nameL] = name + } else { + messages = append(messages, fmt.Sprintf("same target table %v vs %s", nameO, name)) + } + } + + if len(messages) > 0 { + return terror.ErrTaskCheckSameTableName.Generate(messages) + } + + return nil +} + // lightningPrecheckAdaptor implements the importer.PreRestoreInfoGetter interface. type lightningPrecheckAdaptor struct { importer.TargetInfoGetter