package browse
import (
"context"
"errors"
"fmt"
"github.com/dolthub/dolt/go/libraries/doltcore/diff"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
)
// TableDiff summarizes how one table changed in a commit relative to its first
// parent. Row-level diffs are out of scope for v1; only the per-table shape and
// exact row counts are reported.
type TableDiff struct {
Name string
Added bool
Dropped bool
SchemaChanged bool
// RowsAdded, RowsRemoved and RowsModified are exact counts derived from the
// prolly diff. They are 0 when counts could not be computed (e.g. the
// primary key set changed, in which case SchemaChanged is true).
RowsAdded int64
RowsRemoved int64
RowsModified int64
}
// CommitDiff is the per-table summary of a single commit versus its first
// parent (or the empty root, for the initial commit).
type CommitDiff struct {
Hash string
Tables []TableDiff
}
// CommitSummary computes the per-table diff summary of the commit identified by
// hashStr (a commit hash, or any ref resolveCommit accepts) against its first
// parent. For the initial commit the comparison is against an empty root, so
// every table shows as added.
func (db *DB) CommitSummary(ctx context.Context, hashStr string) (*CommitDiff, error) {
commit, err := db.resolveCommit(ctx, hashStr)
if err != nil {
return nil, err
}
toRoot, err := commit.GetRootValue(ctx)
if err != nil {
return nil, fmt.Errorf("browse: root value of %q: %w", hashStr, err)
}
fromRoot, err := db.firstParentRoot(ctx, commit)
if err != nil {
return nil, err
}
deltas, err := diff.GetTableDeltas(ctx, fromRoot, toRoot)
if err != nil {
return nil, fmt.Errorf("browse: table deltas for %q: %w", hashStr, err)
}
h, err := commit.HashOf()
if err != nil {
return nil, fmt.Errorf("browse: hash of %q: %w", hashStr, err)
}
out := &CommitDiff{Hash: h.String(), Tables: make([]TableDiff, 0, len(deltas))}
for _, delta := range deltas {
td, err := tableDiff(ctx, delta)
if err != nil {
return nil, err
}
out.Tables = append(out.Tables, td)
}
return out, nil
}
// firstParentRoot returns the root value of the commit's first parent, or an
// empty root when the commit has no parents (the initial commit).
func (db *DB) firstParentRoot(ctx context.Context, commit *doltdb.Commit) (doltdb.RootValue, error) {
parents, err := commit.ParentHashes(ctx)
if err != nil {
return nil, fmt.Errorf("browse: parent hashes: %w", err)
}
if len(parents) == 0 {
root, err := doltdb.EmptyRootValue(ctx, db.ddb.ValueReadWriter(), db.ddb.NodeStore())
if err != nil {
return nil, fmt.Errorf("browse: empty root: %w", err)
}
return root, nil
}
oc, err := db.ddb.ResolveHash(ctx, parents[0])
if err != nil {
return nil, fmt.Errorf("browse: resolve parent %s: %w", parents[0].String(), err)
}
parent, ok := oc.ToCommit()
if !ok {
return nil, fmt.Errorf("browse: parent commit %s is not resolvable (ghost)", parents[0].String())
}
root, err := parent.GetRootValue(ctx)
if err != nil {
return nil, fmt.Errorf("browse: parent root %s: %w", parents[0].String(), err)
}
return root, nil
}
// tableDiff summarizes a single TableDelta.
func tableDiff(ctx context.Context, delta diff.TableDelta) (TableDiff, error) {
name := delta.ToName.Name
if delta.IsDrop() {
name = delta.FromName.Name
}
td := TableDiff{
Name: name,
Added: delta.IsAdd(),
Dropped: delta.IsDrop(),
}
schemaChanged, err := delta.HasSchemaChanged(ctx)
if err != nil {
return TableDiff{}, fmt.Errorf("browse: schema-changed check for %q: %w", name, err)
}
td.SchemaChanged = schemaChanged
added, removed, modified, err := tableRowStat(ctx, delta)
if err != nil {
if errors.Is(err, diff.ErrPrimaryKeySetChanged) {
// Row counts are undefined when the PK set changed; the schema
// change is already reflected. Leave counts at 0.
return td, nil
}
return TableDiff{}, fmt.Errorf("browse: row stat for %q: %w", name, err)
}
td.RowsAdded = added
td.RowsRemoved = removed
td.RowsModified = modified
return td, nil
}
// tableRowStat drains diff.StatForTableDelta into exact added/removed/modified
// row counts. StatForTableDelta streams one progress message per changed row,
// so we accumulate the per-message Adds/Removes/Changes.
func tableRowStat(ctx context.Context, delta diff.TableDelta) (added, removed, modified int64, err error) {
ch := make(chan diff.DiffStatProgress, 128)
done := make(chan struct{})
var statErr error
go func() {
defer close(done)
statErr = diff.StatForTableDelta(ctx, ch, delta)
close(ch)
}()
var a, r, m uint64
for p := range ch {
a += p.Adds
r += p.Removes
m += p.Changes
}
<-done
if statErr != nil {
return 0, 0, 0, statErr
}
return int64(a), int64(r), int64(m), nil
}