vitessio/vitess · error

more than one target for source table %s: %s and %s

Error message

more than one target for source table %s: %s and %s

What it means

buildReplicatorPlan enforces a 1:1 mapping between a source table (the SendRule.Match key) and a target table. When a second tablePlan resolves to the same source table match key, planning fails rather than silently merging or double-streaming rows to two targets.

Source

Thrown at go/vt/vttablet/tabletmanager/vreplication/table_plan_builder.go:175

			return nil, err
		}
		if rule == nil {
			continue
		}
		colInfos, ok := colInfoMap[tableName]
		if !ok {
			return nil, fmt.Errorf("table %s not found in schema", tableName)
		}
		tablePlan, err := buildTablePlan(tableName, rule, colInfos, lastpk, stats, source, collationEnv, parser, vr.workflowConfig)
		if err != nil {
			return nil, vterrors.Wrapf(err, "failed to build table replication plan for %s table", tableName)
		}
		if tablePlan == nil {
			// Table was excluded.
			continue
		}
		if dup, ok := plan.TablePlans[tablePlan.SendRule.Match]; ok {
			return nil, fmt.Errorf("more than one target for source table %s: %s and %s", tablePlan.SendRule.Match, dup.TargetName, tableName)
		}
		plan.VStreamFilter.Rules = append(plan.VStreamFilter.Rules, tablePlan.SendRule)
		plan.TargetTables[tableName] = tablePlan
		plan.TablePlans[tablePlan.SendRule.Match] = tablePlan
	}
	return plan, nil
}

// MatchTable is similar to tableMatches and buildPlan defined in vstreamer/planbuilder.go.
func MatchTable(tableName string, filter *binlogdatapb.Filter) (*binlogdatapb.Rule, error) {
	for _, rule := range filter.Rules {
		switch {
		case strings.HasPrefix(rule.Match, "/"):
			expr := strings.Trim(rule.Match, "/")
			result, err := regexp.MatchString(expr, tableName)
			if err != nil {
				return nil, err
			}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Review the Filter rules and remove/merge overlapping matches
  2. Make wildcard rules more specific so they don't overlap exact matches
  3. Order rules so the intended one wins and delete the redundant rule (first explicit rule should be the only one matching each table)

Example fix

// before
"rules": [{"match": "t*", "targetTable": "t"}, {"match": "t1", "targetTable": "t1"}]
// after
"rules": [{"match": "t1", "targetTable": "t1"}, {"match": "t2", "targetTable": "t2"}]
Defensive patterns

Strategy: validation

Validate before calling

seen := map[string]string{}
for _, r := range filter.Rules {
    if prev, dup := seen[r.Match]; dup {
        return fmt.Errorf("rule %q duplicates match already handled by %q", r.TargetTable, prev)
    }
    seen[r.Match] = r.TargetTable
}

Type guard

func hasOverlappingRules(rules []*Rule) bool {
    matches := map[string]bool{}
    for _, r := range rules {
        if matches[r.Match] { return true }
        matches[r.Match] = true
    }
    return false
}

Prevention

When it happens

Trigger: Two filter rules whose `match` patterns resolve to the same source table, e.g. one exact rule `t1` plus a wildcard `t*` that also matches t1, both producing a plan for the same SendRule.Match.

Common situations: Overlapping regex rules in a MoveTables Filter; adding a specific-table rule without removing an existing catch-all rule; copy-phase rules built by initTablesForCopy colliding with user-supplied rules.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/f331aa775e14ccc3. Report an issue: GitHub.