[gold] Use SQL DB to fetch diff metrics

Note to reviewer: sql_differ.FillRefDiffs is basically
identical to the other implementation in ref_differ.

Bug: skia:10582, skia:9080
Change-Id: Icdc459fc259b8ad2a2f1b960f3f4a80481a41c4a
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/370737
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/golden/go/search/ref_differ/ref_differ.go b/golden/go/search/ref_differ/ref_differ.go
index 10aead4..feff398 100644
--- a/golden/go/search/ref_differ/ref_differ.go
+++ b/golden/go/search/ref_differ/ref_differ.go
@@ -39,8 +39,8 @@
 	idx       indexer.IndexSearcher
 }
 
-// New returns a *DiffImpl using given types.
-func New(exp expectations.Classifier, diffStore diff.DiffStore, idx indexer.IndexSearcher) *DiffImpl {
+// NewFirestoreImpl returns a *DiffImpl using a Firestore-based DiffStore.
+func NewFirestoreImpl(exp expectations.Classifier, diffStore diff.DiffStore, idx indexer.IndexSearcher) *DiffImpl {
 	return &DiffImpl{
 		exp:       exp,
 		diffStore: diffStore,
@@ -53,8 +53,8 @@
 	paramsByDigest := r.idx.GetParamsetSummaryByTest(iState)[d.Test]
 
 	// TODO(kjlubick) maybe make this use an errgroup
-	posDigests := r.getDigestsWithLabel(d, match, paramsByDigest, rhsQuery, expectations.Positive)
-	negDigests := r.getDigestsWithLabel(d, match, paramsByDigest, rhsQuery, expectations.Negative)
+	posDigests := getDigestsWithLabel(r.exp, d, match, paramsByDigest, rhsQuery, expectations.Positive)
+	negDigests := getDigestsWithLabel(r.exp, d, match, paramsByDigest, rhsQuery, expectations.Negative)
 
 	var err error
 	ret := make(map[common.RefClosest]*frontend.SRDiffDigest, 2)
@@ -105,14 +105,14 @@
 // getDigestsWithLabel return all digests within the given test that
 // have the given label assigned to them and where the parameters
 // listed in 'match' match.
-func (r *DiffImpl) getDigestsWithLabel(s *frontend.SearchResult, match []string, paramsByDigest map[types.Digest]paramtools.ParamSet, rhsQuery paramtools.ParamSet, targetLabel expectations.Label) types.DigestSlice {
+func getDigestsWithLabel(exp expectations.Classifier, s *frontend.SearchResult, match []string, paramsByDigest map[types.Digest]paramtools.ParamSet, rhsQuery paramtools.ParamSet, targetLabel expectations.Label) types.DigestSlice {
 	ret := types.DigestSlice{}
 	for d, digestParams := range paramsByDigest {
 		// Accept all digests that are:  in the set of allowed digests
 		//                              match the target label and where the required
 		//                              parameter fields match.
 		if (len(rhsQuery) == 0 || rhsQuery.Matches(digestParams)) &&
-			(r.exp.Classification(s.Test, d) == targetLabel) &&
+			(exp.Classification(s.Test, d) == targetLabel) &&
 			paramSetsMatch(match, s.ParamSet, digestParams) {
 			ret = append(ret, d)
 		}
diff --git a/golden/go/search/ref_differ/ref_differ_test.go b/golden/go/search/ref_differ/ref_differ_test.go
index d758f8c..7f3b431 100644
--- a/golden/go/search/ref_differ/ref_differ_test.go
+++ b/golden/go/search/ref_differ/ref_differ_test.go
@@ -67,7 +67,7 @@
 			betaNegativeDigest: makeDiffMetric(9),
 		}, nil)
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	matches := []string{types.PrimaryKeyField} // This is the default for several gold queries.
@@ -146,7 +146,7 @@
 			betaNegativeDigest: makeDiffMetric(9),
 		}, nil)
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	matches := []string{types.PrimaryKeyField} // This is the default for several gold queries.
@@ -214,7 +214,7 @@
 		},
 	)
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	matches := []string{types.PrimaryKeyField}
@@ -249,7 +249,7 @@
 
 	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(map[types.TestName]digest_counter.DigestCount{})
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	matches := []string{types.PrimaryKeyField}
@@ -305,7 +305,7 @@
 			gammaPositiveDigest: makeDiffMetric(2),
 		}, nil)
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	matches := []string{"arch", types.PrimaryKeyField} // Only Gamma has x86 in the "arch" values.
@@ -373,7 +373,7 @@
 			alphaPositiveDigest: makeDiffMetric(2),
 		}, nil)
 
-	rd := New(es, mds, mis)
+	rd := NewFirestoreImpl(es, mds, mis)
 
 	metric := query.CombinedMetric
 	input := frontend.SearchResult{
diff --git a/golden/go/search/ref_differ/sql_differ.go b/golden/go/search/ref_differ/sql_differ.go
new file mode 100644
index 0000000..559b773
--- /dev/null
+++ b/golden/go/search/ref_differ/sql_differ.go
@@ -0,0 +1,127 @@
+package ref_differ
+
+import (
+	"context"
+	"math"
+
+	"github.com/jackc/pgx/v4"
+	"github.com/jackc/pgx/v4/pgxpool"
+
+	"go.skia.org/infra/go/paramtools"
+	"go.skia.org/infra/go/skerr"
+	"go.skia.org/infra/golden/go/expectations"
+	"go.skia.org/infra/golden/go/indexer"
+	"go.skia.org/infra/golden/go/search/common"
+	"go.skia.org/infra/golden/go/search/frontend"
+	"go.skia.org/infra/golden/go/search/query"
+	"go.skia.org/infra/golden/go/sql"
+	"go.skia.org/infra/golden/go/types"
+)
+
+type SQLImpl struct {
+	sqlDB *pgxpool.Pool
+	exp   expectations.Classifier
+	idx   indexer.IndexSearcher
+}
+
+func NewSQLImpl(sqlDB *pgxpool.Pool, exp expectations.Classifier, idx indexer.IndexSearcher) *SQLImpl {
+	return &SQLImpl{
+		sqlDB: sqlDB,
+		exp:   exp,
+		idx:   idx,
+	}
+}
+
+// FillRefDiffs implements the RefDiffer interface by querying a SQL database.
+func (s *SQLImpl) FillRefDiffs(ctx context.Context, d *frontend.SearchResult, metric string, match []string, rhsQuery paramtools.ParamSet, iState types.IgnoreState) error {
+	paramsByDigest := s.idx.GetParamsetSummaryByTest(iState)[d.Test]
+
+	// TODO(kjlubick) maybe make this use an errgroup
+	posDigests := getDigestsWithLabel(s.exp, d, match, paramsByDigest, rhsQuery, expectations.Positive)
+	negDigests := getDigestsWithLabel(s.exp, d, match, paramsByDigest, rhsQuery, expectations.Negative)
+
+	var err error
+	ret := make(map[common.RefClosest]*frontend.SRDiffDigest, 2)
+	ret[common.PositiveRef], err = s.getClosestDiff(ctx, metric, d.Digest, posDigests)
+	if err != nil {
+		return skerr.Wrapf(err, "fetching positive diffs")
+	}
+	ret[common.NegativeRef], err = s.getClosestDiff(ctx, metric, d.Digest, negDigests)
+	if err != nil {
+		return skerr.Wrapf(err, "fetching negative diffs")
+	}
+
+	// Find the minimum according to the diff metric.
+	closest := common.NoRef
+	minDiff := float32(math.Inf(1))
+	dCount := s.idx.DigestCountsByTest(iState)[d.Test]
+	for ref, srdd := range ret {
+		if srdd != nil {
+			// Fill in the missing fields.
+			srdd.Status = s.exp.Classification(d.Test, srdd.Digest)
+			srdd.ParamSet = paramsByDigest[srdd.Digest]
+			srdd.OccurrencesInTile = dCount[srdd.Digest]
+
+			// Find the minimum.
+			if srdd.QueryMetric < minDiff {
+				closest = ref
+				minDiff = srdd.QueryMetric
+			}
+		}
+	}
+	d.ClosestRef = closest
+	d.RefDiffs = ret
+	return nil
+}
+
+// getClosestDiff returns the closest diff between a digest and a set of digest. It uses the
+// provided metric as a measure of closeness.
+func (s *SQLImpl) getClosestDiff(ctx context.Context, metric string, left types.Digest, rightDigests types.DigestSlice) (*frontend.SRDiffDigest, error) {
+	if len(rightDigests) == 0 {
+		return nil, nil
+	}
+	const baseStatement = `SELECT encode(right_digest, 'hex'), num_pixels_diff, percent_pixels_diff, max_rgba_diffs,
+combined_metric, dimensions_differ
+FROM DiffMetrics AS OF SYSTEM TIME '-0.1s'
+WHERE left_digest = $1 AND right_digest IN `
+	orderStatement := ` ORDER BY combined_metric ASC LIMIT 1`
+	switch metric {
+	case query.PercentMetric:
+		orderStatement = ` ORDER BY percent_pixels_diff ASC LIMIT 1`
+	case query.PixelMetric:
+		orderStatement = ` ORDER BY num_pixels_diff ASC LIMIT 1`
+	}
+	vp := sql.ValuesPlaceholders(len(rightDigests)+1, 1)
+	arguments := make([]interface{}, 0, len(rightDigests)+1)
+	lb, err := sql.DigestToBytes(left)
+	if err != nil {
+		return nil, skerr.Wrap(err)
+	}
+	arguments = append(arguments, lb)
+	for _, d := range rightDigests {
+		b, err := sql.DigestToBytes(d)
+		if err != nil {
+			return nil, skerr.Wrap(err)
+		}
+		arguments = append(arguments, b)
+	}
+
+	row := s.sqlDB.QueryRow(ctx, baseStatement+vp+orderStatement, arguments...)
+	rv := frontend.SRDiffDigest{}
+	err = row.Scan(&rv.Digest, &rv.NumDiffPixels, &rv.PixelDiffPercent, &rv.MaxRGBADiffs,
+		&rv.CombinedMetric, &rv.DimDiffer)
+	if err == pgx.ErrNoRows {
+		return nil, nil
+	} else if err != nil {
+		return nil, skerr.Wrapf(err, "finding closest diff using %s and %d right digests", left, len(rightDigests))
+	}
+
+	rv.QueryMetric = rv.CombinedMetric
+	switch metric {
+	case query.PercentMetric:
+		rv.QueryMetric = rv.PixelDiffPercent
+	case query.PixelMetric:
+		rv.QueryMetric = float32(rv.NumDiffPixels)
+	}
+	return &rv, nil
+}
diff --git a/golden/go/search/ref_differ/sql_differ_test.go b/golden/go/search/ref_differ/sql_differ_test.go
new file mode 100644
index 0000000..b906bf1
--- /dev/null
+++ b/golden/go/search/ref_differ/sql_differ_test.go
@@ -0,0 +1,447 @@
+package ref_differ
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"go.skia.org/infra/go/paramtools"
+	"go.skia.org/infra/go/testutils/unittest"
+	"go.skia.org/infra/golden/go/digest_counter"
+	"go.skia.org/infra/golden/go/expectations"
+	mock_index "go.skia.org/infra/golden/go/indexer/mocks"
+	"go.skia.org/infra/golden/go/search/common"
+	"go.skia.org/infra/golden/go/search/frontend"
+	"go.skia.org/infra/golden/go/search/query"
+	"go.skia.org/infra/golden/go/sql"
+	"go.skia.org/infra/golden/go/sql/schema"
+	"go.skia.org/infra/golden/go/sql/sqltest"
+	"go.skia.org/infra/golden/go/types"
+)
+
+func TestSQLRefDiffer_PositiveAndNegativeDigestsExist_CombinedMetric_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	existingData := generateSQLDiffs()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
+	waitForSystemTime()
+
+	es := makeClassifier([]types.Digest{posDigestOne, posDigestTwo, posDigestThree}, []types.Digest{negDigestSix, negDigestSeven})
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				posDigestTwo:   arbitraryParamSetForTest(),
+				posDigestThree: arbitraryParamSetForTest(),
+				negDigestSix:   arbitraryParamSetForTest(),
+				negDigestSeven: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				posDigestTwo:   1,
+				posDigestThree: 1,
+				negDigestSix:   1,
+				negDigestSeven: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.CombinedMetric
+	matches := []string{types.PrimaryKeyField} // This is the default for several gold queries.
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     testName,
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.NegativeRef, input.ClosestRef)
+	posRef := input.RefDiffs[common.PositiveRef]
+	require.NotNil(t, posRef)
+	assert.Equal(t, posDigestThree, posRef.Digest)
+	assert.Equal(t, [4]int{9, 10, 11, 12}, posRef.MaxRGBADiffs)
+	assert.Equal(t, float32(1.0), posRef.CombinedMetric)
+	assert.Equal(t, 300, posRef.NumDiffPixels)
+	assert.Equal(t, float32(0.3), posRef.PixelDiffPercent)
+	assert.True(t, posRef.DimDiffer)
+	assert.Equal(t, posRef.CombinedMetric, posRef.QueryMetric)
+
+	negRef := input.RefDiffs[common.NegativeRef]
+	require.NotNil(t, negRef)
+	assert.Equal(t, negDigestSeven, negRef.Digest)
+	assert.Equal(t, [4]int{17, 18, 19, 20}, negRef.MaxRGBADiffs)
+	assert.Equal(t, float32(0.5), negRef.CombinedMetric)
+	assert.Equal(t, 250, negRef.NumDiffPixels)
+	assert.Equal(t, float32(0.25), negRef.PixelDiffPercent)
+	assert.False(t, negRef.DimDiffer)
+	assert.Equal(t, negRef.CombinedMetric, negRef.QueryMetric)
+}
+
+func TestSQLRefDiffer_PositiveAndNegativeDigestsExist_PercentPixels_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	existingData := generateSQLDiffs()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
+	waitForSystemTime()
+
+	es := makeClassifier([]types.Digest{posDigestOne, posDigestTwo, posDigestThree}, []types.Digest{negDigestSix, negDigestSeven})
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				posDigestTwo:   arbitraryParamSetForTest(),
+				posDigestThree: arbitraryParamSetForTest(),
+				negDigestSix:   arbitraryParamSetForTest(),
+				negDigestSeven: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				posDigestTwo:   1,
+				posDigestThree: 1,
+				negDigestSix:   1,
+				negDigestSeven: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.PercentMetric
+	matches := []string{types.PrimaryKeyField}
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     testName,
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.PositiveRef, input.ClosestRef)
+	posRef := input.RefDiffs[common.PositiveRef]
+	require.NotNil(t, posRef)
+	assert.Equal(t, posDigestTwo, posRef.Digest)
+	assert.Equal(t, float32(0.1), posRef.PixelDiffPercent)
+	assert.Equal(t, posRef.PixelDiffPercent, posRef.QueryMetric)
+	negRef := input.RefDiffs[common.NegativeRef]
+	require.NotNil(t, negRef)
+	assert.Equal(t, negDigestSix, negRef.Digest)
+	assert.Equal(t, float32(0.15), negRef.PixelDiffPercent)
+	assert.Equal(t, negRef.PixelDiffPercent, negRef.QueryMetric)
+}
+
+func TestSQLRefDiffer_PositiveAndNegativeDigestsExist_NumPixels_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	existingData := generateSQLDiffs()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
+	waitForSystemTime()
+
+	es := makeClassifier([]types.Digest{posDigestOne, posDigestTwo, posDigestThree}, []types.Digest{negDigestSix, negDigestSeven})
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				posDigestTwo:   arbitraryParamSetForTest(),
+				posDigestThree: arbitraryParamSetForTest(),
+				negDigestSix:   arbitraryParamSetForTest(),
+				negDigestSeven: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				posDigestTwo:   1,
+				posDigestThree: 1,
+				negDigestSix:   1,
+				negDigestSeven: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.PixelMetric
+	matches := []string{types.PrimaryKeyField}
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     testName,
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.PositiveRef, input.ClosestRef)
+	posRef := input.RefDiffs[common.PositiveRef]
+	require.NotNil(t, posRef)
+	assert.Equal(t, posDigestOne, posRef.Digest)
+	assert.Equal(t, 100, posRef.NumDiffPixels)
+	assert.Equal(t, float32(posRef.NumDiffPixels), posRef.QueryMetric)
+	negRef := input.RefDiffs[common.NegativeRef]
+	require.NotNil(t, negRef)
+	assert.Equal(t, negDigestSix, negRef.Digest)
+	assert.Equal(t, 150, negRef.NumDiffPixels)
+	assert.Equal(t, float32(negRef.NumDiffPixels), negRef.QueryMetric)
+}
+
+func TestSQLRefDiffer_NoNegativeDigests_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	existingData := generateSQLDiffs()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
+	waitForSystemTime()
+
+	es := makeClassifier([]types.Digest{posDigestOne, posDigestTwo, posDigestThree}, nil)
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				posDigestTwo:   arbitraryParamSetForTest(),
+				posDigestThree: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				posDigestTwo:   1,
+				posDigestThree: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.CombinedMetric
+	matches := []string{types.PrimaryKeyField}
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     testName,
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.PositiveRef, input.ClosestRef)
+	posRef := input.RefDiffs[common.PositiveRef]
+	require.NotNil(t, posRef)
+	assert.Equal(t, posDigestThree, posRef.Digest)
+
+	negRef := input.RefDiffs[common.NegativeRef]
+	require.Nil(t, negRef)
+}
+
+func TestSQLRefDiffer_NoMatchingDigests_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	existingData := generateSQLDiffs()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
+	waitForSystemTime()
+
+	es := makeClassifier([]types.Digest{posDigestOne}, []types.Digest{negDigestSeven})
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				negDigestSeven: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				negDigestSeven: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.CombinedMetric
+	matches := []string{types.PrimaryKeyField}
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     "The wrong test name that nothing matches",
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.NoRef, input.ClosestRef)
+	assert.Nil(t, input.RefDiffs[common.PositiveRef])
+	assert.Nil(t, input.RefDiffs[common.NegativeRef])
+}
+
+func TestSQLRefDiffer_NoDiffMetrics_Success(t *testing.T) {
+	unittest.LargeTest(t)
+
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+
+	es := makeClassifier([]types.Digest{posDigestOne}, []types.Digest{negDigestSeven})
+	mis := &mock_index.IndexSearcher{}
+	mis.On("GetParamsetSummaryByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]map[types.Digest]paramtools.ParamSet{
+			testName: {
+				posDigestOne:   arbitraryParamSetForTest(),
+				negDigestSeven: arbitraryParamSetForTest(),
+			},
+		},
+	)
+	mis.On("DigestCountsByTest", types.ExcludeIgnoredTraces).Return(
+		map[types.TestName]digest_counter.DigestCount{
+			testName: {
+				posDigestOne:   1,
+				negDigestSeven: 1,
+			},
+		},
+	)
+
+	rd := NewSQLImpl(db, es, mis)
+
+	metric := query.CombinedMetric
+	matches := []string{types.PrimaryKeyField}
+	input := frontend.SearchResult{
+		ParamSet: arbitraryParamSetForTest(),
+		Digest:   untriagedDigestZero,
+		Test:     testName,
+	}
+	err := rd.FillRefDiffs(context.Background(), &input, metric, matches, matchAll, types.ExcludeIgnoredTraces)
+
+	require.NoError(t, err)
+	assert.Equal(t, common.NoRef, input.ClosestRef)
+	assert.Nil(t, input.RefDiffs[common.PositiveRef])
+	assert.Nil(t, input.RefDiffs[common.NegativeRef])
+}
+
+func makeClassifier(positive []types.Digest, negative []types.Digest) expectations.Classifier {
+	var exp expectations.Expectations
+	for _, p := range positive {
+		exp.Set(testName, p, expectations.Positive)
+	}
+	for _, n := range negative {
+		exp.Set(testName, n, expectations.Negative)
+	}
+	return &exp
+}
+
+// The data generated here is nonsensical in the sense that the numbers could not correspond to
+// real images. However, it is still useful because it has different orderings for different
+// metric types.
+func generateSQLDiffs() schema.Tables {
+	now := time.Date(2021, time.February, 3, 4, 5, 6, 0, time.UTC)
+	return schema.Tables{DiffMetrics: []schema.DiffMetricRow{{
+		LeftDigest:        mustDigestToBytes(untriagedDigestZero),
+		RightDigest:       mustDigestToBytes(posDigestOne),
+		NumPixelsDiff:     100,
+		PercentPixelsDiff: 0.2,
+		MaxRGBADiffs:      [4]int{1, 2, 3, 4},
+		MaxChannelDiff:    4,
+		CombinedMetric:    3.0,
+		DimensionsDiffer:  false,
+		Timestamp:         now,
+	}, {
+		LeftDigest:        mustDigestToBytes(untriagedDigestZero),
+		RightDigest:       mustDigestToBytes(posDigestTwo),
+		NumPixelsDiff:     200,
+		PercentPixelsDiff: 0.1,
+		MaxRGBADiffs:      [4]int{5, 6, 7, 8},
+		MaxChannelDiff:    8,
+		CombinedMetric:    2.0,
+		DimensionsDiffer:  false,
+		Timestamp:         now,
+	}, {
+		LeftDigest:        mustDigestToBytes(untriagedDigestZero),
+		RightDigest:       mustDigestToBytes(posDigestThree),
+		NumPixelsDiff:     300,
+		PercentPixelsDiff: 0.3,
+		MaxRGBADiffs:      [4]int{9, 10, 11, 12},
+		MaxChannelDiff:    12,
+		CombinedMetric:    1.0,
+		DimensionsDiffer:  true,
+		Timestamp:         now,
+	}, {
+		LeftDigest:        mustDigestToBytes(untriagedDigestZero),
+		RightDigest:       mustDigestToBytes(negDigestSix),
+		NumPixelsDiff:     150,
+		PercentPixelsDiff: .15,
+		MaxRGBADiffs:      [4]int{13, 14, 15, 16},
+		MaxChannelDiff:    16,
+		CombinedMetric:    6,
+		DimensionsDiffer:  true,
+		Timestamp:         now,
+	}, {
+		LeftDigest:        mustDigestToBytes(untriagedDigestZero),
+		RightDigest:       mustDigestToBytes(negDigestSeven),
+		NumPixelsDiff:     250,
+		PercentPixelsDiff: .250,
+		MaxRGBADiffs:      [4]int{17, 18, 19, 20},
+		MaxChannelDiff:    20,
+		CombinedMetric:    0.5,
+		DimensionsDiffer:  false,
+		Timestamp:         now,
+	}}}
+}
+
+const (
+	untriagedDigestZero = types.Digest("00000000000000000000000000000000")
+	posDigestOne        = types.Digest("11111111111111111111111111111111")
+	posDigestTwo        = types.Digest("22222222222222222222222222222222")
+	posDigestThree      = types.Digest("33333333333333333333333333333333")
+	negDigestSix        = types.Digest("66666666666666666666666666666666")
+	negDigestSeven      = types.Digest("77777777777777777777777777777777")
+)
+
+func arbitraryParamSetForTest() paramtools.ParamSet {
+	return paramtools.ParamSet{
+		types.PrimaryKeyField: []string{string(testName)},
+		"arbitrary":           []string{"data"},
+		"does":                []string{"not", "matter"},
+	}
+}
+
+func mustDigestToBytes(d types.Digest) schema.DigestBytes {
+	db, err := sql.DigestToBytes(d)
+	if err != nil {
+		panic(err)
+	}
+	return db
+}
+
+// waitForSystemTime waits for a time greater than the duration mentioned in "AS OF SYSTEM TIME"
+// clauses in queries. This way, the queries will be accurate.
+func waitForSystemTime() {
+	time.Sleep(150 * time.Millisecond)
+}
diff --git a/golden/go/search/search.go b/golden/go/search/search.go
index ad84b1a..d2cb765 100644
--- a/golden/go/search/search.go
+++ b/golden/go/search/search.go
@@ -789,7 +789,13 @@
 func (s *SearchImpl) getReferenceDiffs(ctx context.Context, resultDigests []*frontend.SearchResult, metric string, match []string, rhsQuery paramtools.ParamSet, is types.IgnoreState, exp expectations.Classifier, idx indexer.IndexSearcher) error {
 	ctx, span := trace.StartSpan(ctx, "search.getReferenceDiffs")
 	defer span.End()
-	refDiffer := ref_differ.New(exp, s.diffStore, idx)
+	var refDiffer ref_differ.RefDiffer
+	if useSQLDiffMetrics(ctx) {
+		refDiffer = ref_differ.NewSQLImpl(s.sqlDB, exp, idx)
+	} else {
+		refDiffer = ref_differ.NewFirestoreImpl(exp, s.diffStore, idx)
+	}
+
 	errGroup, gCtx := errgroup.WithContext(ctx)
 	sklog.Infof("Going to spawn %d goroutines to get reference diffs", len(resultDigests))
 	for _, retDigest := range resultDigests {
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 1b4bd28..5b8fe40 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -552,6 +552,9 @@
 	}
 	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
 	defer cancel()
+	if r.Form.Get("use_sql") != "" {
+		ctx = context.WithValue(ctx, search.UseSQLDiffMetricsKey, true)
+	}
 
 	searchResponse, err := wh.SearchAPI.Search(ctx, q)
 	if err != nil {
@@ -1053,9 +1056,13 @@
 		return
 	}
 	testName := testNames[0]
+	ctx := r.Context()
+	if r.Form.Get("use_sql") != "" {
+		ctx = context.WithValue(ctx, search.UseSQLDiffMetricsKey, true)
+	}
 
 	idx := wh.Indexer.GetIndex()
-	searchResponse, err := wh.SearchAPI.Search(r.Context(), &q)
+	searchResponse, err := wh.SearchAPI.Search(ctx, &q)
 	if err != nil {
 		httputils.ReportError(w, err, "Search for digests failed.", http.StatusInternalServerError)
 		return