@@ 0,0 1,90 @@
+package browse
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// tableHash is a helper for the common "the table must exist here" case.
+func tableHash(t *testing.T, db *DB, ctx context.Context, refStr, table string) string {
+ t.Helper()
+ h, ok, err := db.TableHash(ctx, refStr, table)
+ require.NoError(t, err)
+ require.True(t, ok, "table %q must exist at %q", table, refStr)
+ require.NotEmpty(t, h)
+ return h
+}
+
+func TestTableHashStable(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ first := tableHash(t, db, ctx, "main", "users")
+ second := tableHash(t, db, ctx, "main", "users")
+ assert.Equal(t, first, second, "the same table at the same ref must hash the same")
+
+ // A branch and the commit hash it points at are the same root, so they must
+ // produce the same table hash. dev's head is C2.
+ byBranch := tableHash(t, db, ctx, "dev", "users")
+ byCommit := tableHash(t, db, ctx, commitHash(t, msgInsert), "users")
+ assert.Equal(t, byBranch, byCommit, "branch and its head commit must agree")
+}
+
+// TestTableHashDetectsChange is the property the whole history walk rests on:
+// the hash changes exactly when the table does.
+func TestTableHashDetectsChange(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ atInsert := tableHash(t, db, ctx, commitHash(t, msgInsert), "users") // C2
+ atModify := tableHash(t, db, ctx, commitHash(t, msgModify), "users") // C3: rows changed
+ atAddItem := tableHash(t, db, ctx, commitHash(t, msgAddItem), "users")
+
+ assert.NotEqual(t, atInsert, atModify, "C3 changed users rows, hash must differ")
+ // C4 created items and docs and left users alone: a walk must be able to
+ // skip it without reading a row.
+ assert.Equal(t, atAddItem, atModify, "C4 did not touch users, hash must be unchanged")
+}
+
+func TestTableHashMissingTable(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ // items is created by C4, so it does not exist at C2 — an ordinary answer
+ // for a backwards walk, not an error.
+ h, ok, err := db.TableHash(ctx, commitHash(t, msgInsert), "items")
+ require.NoError(t, err)
+ assert.False(t, ok)
+ assert.Empty(t, h)
+
+ // A table that never existed behaves the same way.
+ h, ok, err = db.TableHash(ctx, "main", "no-such-table")
+ require.NoError(t, err)
+ assert.False(t, ok)
+ assert.Empty(t, h)
+}
+
+func TestTableHashRefNotFound(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ _, ok, err := db.TableHash(ctx, "no-such-ref", "users")
+ require.ErrorIs(t, err, ErrRefNotFound)
+ assert.False(t, ok)
+}
+
+func TestTableHashDistinctTables(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ users := tableHash(t, db, ctx, "main", "users")
+ items := tableHash(t, db, ctx, "main", "items")
+ docs := tableHash(t, db, ctx, "main", "docs")
+
+ assert.NotEqual(t, users, items)
+ assert.NotEqual(t, users, docs)
+ assert.NotEqual(t, items, docs, "an empty table must not collide with another table")
+}
@@ 93,6 93,46 @@ func (db *DB) Tables(ctx context.Context, refStr string) ([]TableInfo, error) {
return infos, nil
}
+// TableHash returns the content hash of one table in the committed root at ref
+// (a branch name or a commit hash). It reads no rows: the hash is the address
+// of the table struct itself, so answering "did this table change between two
+// commits?" costs a root lookup and nothing more. That is what makes a history
+// walk affordable — a walk that has to attribute a change to a commit can skip
+// every commit whose table hash equals its neighbour's, and read rows only at
+// the few commits that actually touched the table.
+//
+// The hash covers the whole table (schema, row data, secondary indexes), not
+// just the rows. For change detection that is the wanted answer: a column added
+// without touching a row is still a change to the table.
+//
+// A table that does not exist at ref is ok=false with a nil error, deliberately
+// *not* ErrTableNotFound (which Rows returns). The caller of this primitive is
+// a loop walking backwards through history asking "did it change?", and a table
+// that had not been created yet at an old commit is an ordinary answer there,
+// not a failure. Callers that need "absent" to be an error can test ok
+// themselves; a caller that walked into an error at every pre-creation commit
+// could not tell that case apart from a real one.
+func (db *DB) TableHash(ctx context.Context, refStr, table string) (string, bool, error) {
+ root, err := db.resolveRoot(ctx, refStr)
+ if err != nil {
+ return "", false, err
+ }
+
+ tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: table})
+ if err != nil {
+ return "", false, fmt.Errorf("browse: get table %q at %q: %w", table, refStr, err)
+ }
+ if !ok {
+ return "", false, nil
+ }
+
+ h, err := tbl.HashOf()
+ if err != nil {
+ return "", false, fmt.Errorf("browse: hash of table %q at %q: %w", table, refStr, err)
+ }
+ return h.String(), true, nil
+}
+
// columnInfos renders a schema's columns in natural table order.
func columnInfos(sch schema.Schema) []ColumnInfo {
cols := sch.GetAllCols().GetColumns()