vitessio/vitess · error

plan not found for table: %s, current plans are: %#v

Error message

plan not found for table: %s, current plans are: %#v

What it means

vcopier.copyTable processes a binlog RowEvent for a table being copied, but the copy plan (built from the workflow's Filter) has no entry under plan.TargetTables for that table name. The controller only expects events for tables it planned to copy, so an unmatched table name indicates a plan/event mismatch and the copy phase fails.

Source

Thrown at go/vt/vttablet/tabletmanager/vreplication/vcopier.go:395

// copyTable performs the synchronized copy of the next set of rows from
// the current table being copied. Each packet received is transactionally
// committed with the lastpk. This allows for consistent resumability.
func (vc *vcopier) copyTable(ctx context.Context, tableName string, copyState map[string]*sqltypes.Result) error {
	defer vc.vr.dbClient.Rollback()
	defer vc.vr.stats.PhaseTimings.Record("copy", time.Now())
	defer vc.vr.stats.CopyLoopCount.Add(1)

	log.Info(fmt.Sprintf("Copying table %s, lastpk: %v", tableName, copyState[tableName]))

	plan, err := vc.vr.buildReplicatorPlan(vc.vr.source, vc.vr.colInfoMap, nil, vc.vr.stats, vc.vr.vre.env.CollationEnv(), vc.vr.vre.env.Parser())
	if err != nil {
		return err
	}

	initialPlan, ok := plan.TargetTables[tableName]
	if !ok {
		return fmt.Errorf("plan not found for table: %s, current plans are: %#v", tableName, plan.TargetTables)
	}

	// Save the controller-level context before applying the copy phase
	// duration timeout to it. Its cancellation means that the controller
	// is stopping -- because the workflow is being stopped or deleted, or
	// the engine is closing -- and any in-flight post copy action must
	// then be interrupted, whereas an elapsed copy phase duration must
	// not interrupt it.
	stopCtx := ctx
	ctx, cancel := context.WithTimeout(ctx, vc.vr.workflowConfig.CopyPhaseDuration)
	defer cancel()

	var lastpkpb *querypb.QueryResult
	if lastpkqr := copyState[tableName]; lastpkqr != nil {
		lastpkpb = sqltypes.ResultToProto3(lastpkqr)
	}

	rowsCopiedTicker := time.NewTicker(rowsCopiedUpdateInterval)

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Check the workflow's TargetTables in the plan log and confirm the offending table should be copied
  2. Update the workflow Filter send-rules to explicitly include or exclude the table, then restart the copy phase
  3. If the table was created mid-copy, recreate the workflow so the plan includes it
  4. Verify consistent table name casing between source MySQL and the filter rules
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the table is covered by the workflow filter before copy
-- compare source table list against the workflow's TargetTables / send-rules

Try / catch

if err := vc.copyNext(ctx); err != nil {
  if strings.Contains(err.Error(), "plan not found for table") {
    // table appeared mid-copy or filter mismatch: rebuild workflow
    // reload the plan / recreate the workflow with corrected rules
  }
  return err
}

Prevention

When it happens

Trigger: A TableMap/RowEvent arrives during the copy phase for a table not in the workflow's TargetTables — e.g. the source schema gained a new table after planning, a send-rule match is broader than the planned tables, or table name casing differs.

Common situations: A table is created on the source mid-copy; filter send-rule wildcards (e.g. 't.*') match more tables than the plan enumerated; keyspace/table name case mismatch between MySQL and Vitess.

Related errors


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