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
16 changes: 0 additions & 16 deletions go/vt/vttablet/tabletmanager/vdiff/primitive_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,3 @@ func (pe *primitiveExecutor) next() ([]sqltypes.Value, error) {
pe.rows = pe.rows[1:]
return row, nil
}

// drain fastforward's a shard to process (and ignore) everything from its results stream and return a count of the
// discarded rows.
func (pe *primitiveExecutor) drain(ctx context.Context) (int64, error) {
var count int64
for {
row, err := pe.next()
if err != nil {
return 0, err
}
if row == nil {
return count, nil
}
count++
}
}
16 changes: 16 additions & 0 deletions go/vt/vttablet/tabletmanager/vdiff/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ type DiffMismatch struct {
type RowDiff struct {
Row map[string]string `json:"Row,omitempty"`
Query string `json:"Query,omitempty"`
// LosslessValues is set when the sample contains all of the row's column
// values without truncation, meaning it can be used to prove that two
// rows are identical during extra-row reconciliation. The marker is
// deliberately affirmative: samples that are lossy (only-pks, truncated
// values) -- or that were persisted by an older binary and reloaded on
// resume -- lack it and are excluded from reconciliation.
LosslessValues bool `json:"LosslessValues,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a release-note callout for the VDiff behavior change

This changes user-visible VDiff reconciliation results and also adds LosslessValues to serialized row samples, but the commit contains no release/deployment-note update. Add the required callout so operators and report consumers are informed of the changed behavior and JSON output.

AGENTS.md reference: AGENTS.md:L232-L234

Useful? React with 👍 / 👎.

}

func (td *tableDiffer) genRowDiff(queryStmt string, row []sqltypes.Value, opts *tabletmanagerdatapb.VDiffReportOptions) (*RowDiff, error) {
Expand All @@ -81,13 +88,15 @@ func (td *tableDiffer) genRowDiff(queryStmt string, row []sqltypes.Value, opts *
rd.Query = td.genDebugQueryDiff(sel, row, opts.GetOnlyPks())
}

truncated := false
addVal := func(index int, truncateAt int) error {
buf := sqlparser.NewTrackedBuffer(nil)
sel.SelectExprs.Exprs[index].Format(buf)
col := buf.String()
// Let's truncate if it's really worth it to avoid losing
// value for a few chars.
if truncateAt > 0 && row[index].Len() >= truncateAt+len(truncatedNotation)+20 {
truncated = true
if row[index].IsBinary() {
rb, err := row[index].ToBytes()
if err != nil { // Should never happen
Expand Down Expand Up @@ -126,6 +135,9 @@ func (td *tableDiffer) genRowDiff(queryStmt string, row []sqltypes.Value, opts *
}

if opts.GetOnlyPks() {
// A PK-only sample is still lossless when the PK columns cover the
// entire projection, since PK values are never truncated.
rd.LosslessValues = len(pks) == len(sel.SelectExprs.Exprs)
return rd, nil
}

Expand All @@ -138,6 +150,10 @@ func (td *tableDiffer) genRowDiff(queryStmt string, row []sqltypes.Value, opts *
}
}

// The sample contains all of the row's column values (this point is not
// reached with only-pks); it is lossless if none of them were truncated.
rd.LosslessValues = !truncated

return rd, nil
}

Expand Down
2 changes: 2 additions & 0 deletions go/vt/vttablet/tabletmanager/vdiff/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func TestGenRowDiff(t *testing.T) {
},
reportOptions: &tabletmanagerdatapb.VDiffReportOptions{},
want: &RowDiff{
LosslessValues: true,
Row: map[string]string{ // The two PK cols should be first
// mysql> select hex("hi4");
// +------------+
Expand Down Expand Up @@ -131,6 +132,7 @@ func TestGenRowDiff(t *testing.T) {
DebugQuery: true,
},
want: &RowDiff{
LosslessValues: true,
Row: map[string]string{
"c1": "1",
"c2": "2",
Expand Down
82 changes: 57 additions & 25 deletions go/vt/vttablet/tabletmanager/vdiff/table_differ.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,14 @@ func (td *tableDiffer) diff(ctx context.Context, coreOpts *tabletmanagerdatapb.V
maxReportSampleRows := reportOpts.GetMaxSampleRows()

for {
lastProcessedRow = sourceRow
// Only advance the persisted position when the previous iteration
// consumed the held source row (advanceSource still holds that
// iteration's decision here). After an extra-target-row iteration the
// held source row has not been processed yet, and recording it as
// lastpk would make a resumed diff skip it permanently.
if advanceSource {
lastProcessedRow = sourceRow
Comment on lines +589 to +590
}

select {
case <-ctx.Done():
Expand Down Expand Up @@ -629,35 +636,60 @@ func (td *tableDiffer) diff(ctx context.Context, coreOpts *tabletmanagerdatapb.V
advanceSource = true
advanceTarget = true
if sourceRow == nil {
diffRow, err := td.genRowDiff(td.tablePlan.sourceQuery, targetRow, reportOpts)
if err != nil {
return nil, vterrors.Wrap(err, "unexpected error generating diff")
}
dr.ExtraRowsTargetDiffs = append(dr.ExtraRowsTargetDiffs, diffRow)

// Drain target, update count.
count, err := targetExecutor.drain(ctx)
if err != nil {
return nil, err
// No more rows from the source; drain the remaining target rows,
// saving a sample for each one (up to maxExtraRowsToCompare) so that
// reconcileExtraRows can match them against any extra source rows.
// Counting drained rows without saving a sample makes them impossible
// to reconcile, producing false positive extra rows in the report.
// The drained rows are merged into the report only after the full
// drain succeeds: they are beyond the persisted lastpk, so partially
// counted rows would be counted again when a failed diff is resumed.
drainedRows := int64(0)
var drainedDiffs []*RowDiff
for targetRow != nil {
if dr.ExtraRowsTarget+drainedRows < maxExtraRowsToCompare {
diffRow, err := td.genRowDiff(td.tablePlan.targetQuery, targetRow, reportOpts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize drained target samples to the source projection

When a workflow projection transforms or renames columns, such as select c0 as c1, buildTablePlan preserves c0 as c1 in sourceQuery but emits c1 in targetQuery, and genRowDiff uses those formatted expressions as the Row map keys. Generating drained target samples from targetQuery therefore makes them unequal to otherwise identical source samples during the new reflect.DeepEqual(...Row...) reconciliation, so the drained-row fix still reports false extras for renamed, aggregate, and time-zone-adjusted projections; use a common projection/key representation for both sides.

Useful? React with 👍 / 👎.

if err != nil {
return nil, vterrors.Wrap(err, "unexpected error generating diff")
}
drainedDiffs = append(drainedDiffs, diffRow)
}
drainedRows++
targetRow, err = targetExecutor.next()
if err != nil {
return nil, err
}
}
dr.ExtraRowsTarget += 1 + count
dr.ProcessedRows += 1 + count
dr.ExtraRowsTarget += drainedRows
dr.ProcessedRows += drainedRows
dr.ExtraRowsTargetDiffs = append(dr.ExtraRowsTargetDiffs, drainedDiffs...)
return dr, nil
}
if targetRow == nil {
// No more rows from the target but we know we have more rows from
// source, so drain them and update the counts.
diffRow, err := td.genRowDiff(td.tablePlan.sourceQuery, sourceRow, reportOpts)
if err != nil {
return nil, vterrors.Wrap(err, "unexpected error generating diff")
}
dr.ExtraRowsSourceDiffs = append(dr.ExtraRowsSourceDiffs, diffRow)
count, err := sourceExecutor.drain(ctx)
if err != nil {
return nil, err
// No more rows from the target; drain the remaining source rows,
// saving a sample for each one (up to maxExtraRowsToCompare) so that
// reconcileExtraRows can match them against any extra target rows.
// As above, the drained rows are merged into the report only after
// the full drain succeeds.
drainedRows := int64(0)
var drainedDiffs []*RowDiff
for sourceRow != nil {
if dr.ExtraRowsSource+drainedRows < maxExtraRowsToCompare {
diffRow, err := td.genRowDiff(td.tablePlan.sourceQuery, sourceRow, reportOpts)
if err != nil {
return nil, vterrors.Wrap(err, "unexpected error generating diff")
}
drainedDiffs = append(drainedDiffs, diffRow)
}
drainedRows++
sourceRow, err = sourceExecutor.next()
if err != nil {
return nil, err
}
}
dr.ExtraRowsSource += 1 + count
dr.ProcessedRows += 1 + count
dr.ExtraRowsSource += drainedRows
dr.ProcessedRows += drainedRows
dr.ExtraRowsSourceDiffs = append(dr.ExtraRowsSourceDiffs, drainedDiffs...)
Comment on lines +690 to +692

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Checkpoint successfully drained source rows

When the target is exhausted immediately after an extra-target comparison, sourceRow is still held and the new advanceSource guard leaves lastProcessedRow at the preceding row (or nil), while this block successfully drains that held row and merges its count and sample into dr. If reconciliation, mismatch persistence, or the completed-state update subsequently fails in diffTable, the deferred progress update stores the advanced report without the corresponding source lastpk; resuming then streams those source rows again and double-counts them. Update the checkpoint to the final successfully drained source row atomically with merging drainedRows.

AGENTS.md reference: AGENTS.md:L119-L124

Useful? React with 👍 / 👎.

return dr, nil
}

Expand Down
Loading
Loading