{"record":{"id":"f9c3daedd4e0229a","repo":"vitessio/vitess","slug":"cannot-build-a-row-streamer-plan-for-the-s-table","errorCode":null,"errorMessage":"cannot build a row streamer plan for the %s table as a lastpk value was provided (%v) and the number of primary key values within it (%d) does not match the number of primary key columns in the table (%d)","messagePattern":"cannot build a row streamer plan for the (.+?) table as a lastpk value was provided \\((.+?)\\) and the number of primary key values within it \\((.+?)\\) does not match the number of primary key columns in the table \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/vttablet/tabletserver/vstreamer/rowstreamer.go","lineNumber":293,"sourceCode":"\t// If we know the index name that we should be using then tell MySQL\n\t// to use it if possible. This helps to ensure that we are able to\n\t// leverage the ordering from the index itself and avoid having to\n\t// do a FILESORT of all the results. This index should contain all\n\t// of the PK columns which are used in the ORDER BY clause below.\n\tvar indexHint string\n\t// If we're pushing down any expressions, we need to let the optimizer\n\t// choose the best index to use.\n\tif st.PKIndexName != \"\" && len(rs.plan.whereExprsToPushDown) == 0 {\n\t\tescapedPKIndexName, err := sqlescape.EnsureEscaped(st.PKIndexName)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tindexHint = fmt.Sprintf(\" force index (%s)\", escapedPKIndexName)\n\t}\n\tbuf.Myprintf(\" from %v%s\", sqlparser.NewIdentifierCS(rs.plan.Table.Name), indexHint)\n\tif len(rs.lastpk) != 0 { // We're in the Nth copy phase cycle and need to resume\n\t\tif len(rs.lastpk) != len(rs.pkColumns) {\n\t\t\treturn \"\", fmt.Errorf(\"cannot build a row streamer plan for the %s table as a lastpk value was provided (%v) and the number of primary key values within it (%d) does not match the number of primary key columns in the table (%d)\",\n\t\t\t\tst.Name, rs.lastpk, len(rs.lastpk), len(rs.pkColumns))\n\t\t}\n\t\tbuf.WriteString(\" where \")\n\t\t// This closure handles the case for composite PKs. For example,\n\t\t// if lastpk was (1,2), the where clause would be:\n\t\t// (col1 = 1 and col2 > 2) or (col1 > 1).\n\t\t// A tuple inequality like (col1,col2) > (1,2) ends up\n\t\t// being a full table scan for MySQL.\n\t\taddLastPKExpressions := func() {\n\t\t\tprefix := \"\"\n\t\t\tfor lastcol, pkCol := range slices.Backward(rs.pkColumns) {\n\t\t\t\tbuf.Myprintf(\"%s(\", prefix)\n\t\t\t\tprefix = \" or \"\n\t\t\t\tfor i, pk := range rs.pkColumns[:lastcol] {\n\t\t\t\t\tbuf.Myprintf(\"%v = \", sqlparser.NewIdentifierCI(rs.plan.Table.Fields[pk].Name))\n\t\t\t\t\trs.lastpk[i].EncodeSQL(buf)\n\t\t\t\t\tbuf.Myprintf(\" and \")\n\t\t\t\t}","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vttablet/tabletserver/vstreamer/rowstreamer.go#L275-L311","documentation":"When resuming a copy phase, the saved lastpk must have exactly one value per primary key column of the table. buildSelect rejects the plan when the counts differ, because the WHERE clause (col1 = ? and col2 > ?) or (col1 > ?) cannot be constructed correctly.","triggerScenarios":"Calling Stream/VStreamRows with a TableLastPK whose lastpk values count differs from the table's PK column count — e.g. lastpk saved against an older schema whose PK had a different number of columns.","commonSituations":"Workflow checkpoint (vgtid TableLastPK) written before an ALTER TABLE changed the primary key; copying lastpk state between tables with different PKs; manual editing of VReplication state.","solutions":["Check the table's current PK column count (SHOW INDEX/SHOW CREATE TABLE) and compare with the checkpointed lastpk in the workflow's vgtid.","If the PK changed, reset/restart the workflow's copy phase (or correct the TableLastPK) so lastpk is regenerated against the current schema.","Do not hand-edit checkpoints; use vtctld workflow restart/copy-phase reset so lastpk is rebuilt from live schema."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"pkCols := getPrimaryKeyColumns(tableName)\nlastpk := checkpoint.TableLastPK[tableName].Lastpk\nif lastpk != nil && len(lastpk.Rows) > 0 && len(lastpk.Rows[0]) != len(pkCols) {\n    return fmt.Errorf(\"stale lastpk for %s: %d values vs %d pk cols; reset copy phase\", tableName, len(lastpk.Rows[0]), len(pkCols))\n}","typeGuard":null,"tryCatchPattern":"err := stream(ctx, filter, tablePKs)\nif err != nil && strings.Contains(err.Error(), \"does not match the number of primary key columns\") {\n    // drop the stale checkpoint and restart the copy phase\n    restartWorkflowCopyPhase(ctx, workflowID)\n}","preventionTips":["Never hand-edit VReplication checkpoints (TableLastPK/vgtid)","Re-plan workflows after any ALTER TABLE touching the primary key","Validate lastpk shape against current schema before resuming streams","Use vtctld workflow reset instead of manual state surgery"],"tags":["vstreamer","rowstreamer","lastpk","copy-phase"],"backgroundTag":"lastpk-pk-column-count-mismatch","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}