~bigbes/sr-ht-dolt

ref: 97e0f8a7d96859c2749e0bd2a59a37dc26eacc2f sr-ht-dolt/browse/diff.go -rw-r--r-- 4.7 KiB
97e0f8a7 — Eugene Blikh browse: add TableHash for per-commit change detection 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
}