[gold] Add filtering results by key.
Filtering by options will come in a followup CL.
Bug: skia:10582
Change-Id: Ib975f5760f4070388aadd6b19c836a493156c32b
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/402426
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/go/ignore/sqlignorestore/util.go b/golden/go/ignore/sqlignorestore/util.go
index 170f294..19e802e 100644
--- a/golden/go/ignore/sqlignorestore/util.go
+++ b/golden/go/ignore/sqlignorestore/util.go
@@ -15,6 +15,7 @@
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
+ "go.skia.org/infra/golden/go/sql"
"go.skia.org/infra/golden/go/sql/schema"
)
@@ -237,7 +238,7 @@
rule.Normalize()
keys := make([]string, 0, len(rule))
for key := range rule {
- if key != sanitize(key) {
+ if key != sql.Sanitize(key) {
sklog.Infof("key %q did not pass sanitization", key)
continue
}
@@ -251,7 +252,7 @@
if j != 0 {
statement += "\tUNION\n"
}
- statement += fmt.Sprintf("\tSELECT trace_id FROM %s WHERE keys -> '%s' = '%q'\n", table, key, sanitize(value))
+ statement += fmt.Sprintf("\tSELECT trace_id FROM %s WHERE keys -> '%s' = '%q'\n", table, key, sql.Sanitize(value))
}
if i == len(keys)-1 {
statement += ")\n"
@@ -270,12 +271,6 @@
return statement
}
-// sanitize removes unsafe characters from strings to avoid SQL injections. Namely quotes.
-func sanitize(s string) string {
- s = strings.ReplaceAll(s, `'`, ``)
- return strings.ReplaceAll(s, `"`, ``)
-}
-
// updateNullTraces updates all traces where matches_any_ignore_rule is NULL to be false, since
// we have not matched any other ignore rules. There is a limit to how many rows will be updated
// with this to avoid transactions from being too big.
diff --git a/golden/go/search/frontend/types.go b/golden/go/search/frontend/types.go
index 80334f9..788efb0 100644
--- a/golden/go/search/frontend/types.go
+++ b/golden/go/search/frontend/types.go
@@ -51,10 +51,11 @@
// TriageHistory is a history of all the times the primary digest has been retriaged for the
// given Test.
TriageHistory []TriageHistory `json:"triage_history"`
- // ParamSet is all the keys and options of all traces that produce the primary digest. It is
- // for frontend UI presentation only; essentially a word cloud of what drew the primary digest.
+ // ParamSet is all the keys and options of all traces that produce the primary digest and
+ // match the given search constraints. It is for frontend UI presentation only; essentially a
+ // word cloud of what drew the primary digest.
// TODO(kjlubick) Consider splitting this into keys and options so it's more clear which are
- // affecting the trace.
+ // affecting the trace.
ParamSet paramtools.ParamSet `json:"paramset"`
// TraceGroup represents all traces that produced this digest at least once in the sliding window
// of commits.
diff --git a/golden/go/search2/search2.go b/golden/go/search2/search2.go
index 12dbe79..ffe2df1 100644
--- a/golden/go/search2/search2.go
+++ b/golden/go/search2/search2.go
@@ -6,6 +6,7 @@
"bytes"
"context"
"encoding/hex"
+ "fmt"
"sort"
"sync"
"time"
@@ -432,8 +433,8 @@
if err != nil {
return nil, skerr.Wrap(err)
}
+ // Fill in the paramsets of the reference images.
for _, sr := range results {
- sr.ParamSet = paramsetsByDigest[sr.Digest]
for _, srdd := range sr.RefDiffs {
if srdd != nil {
srdd.ParamSet = paramsetsByDigest[srdd.Digest]
@@ -532,22 +533,19 @@
func (s *Impl) getMatchingDigestsAndTraces(ctx context.Context) ([]stageOneResult, error) {
ctx, span := trace.StartSpan(ctx, "getMatchingDigestsAndTraces")
defer span.End()
- const statement = `WITH
-UntriagedDigests AS (
+ statement := `WITH
+MatchingDigests AS (
SELECT grouping_id, digest FROM Expectations
WHERE label = ANY($1)
-),
-NonIgnoredTraces AS (
- SELECT trace_id, grouping_id, digest FROM ValuesAtHead
- WHERE most_recent_commit_id >= $2 AND
- corpus = $3 AND
- matches_any_ignore_rule = ANY($4)
-)
-SELECT trace_id, UntriagedDigests.grouping_id, NonIgnoredTraces.digest FROM
-UntriagedDigests
+),`
+ tracesBlock, args := matchingTracesStatement(ctx)
+ statement += tracesBlock
+ statement += `
+SELECT trace_id, MatchingDigests.grouping_id, MatchingTraces.digest FROM
+MatchingDigests
JOIN
-NonIgnoredTraces ON UntriagedDigests.grouping_id = NonIgnoredTraces.grouping_id AND
- UntriagedDigests.digest = NonIgnoredTraces.digest`
+MatchingTraces ON MatchingDigests.grouping_id = MatchingTraces.grouping_id AND
+ MatchingDigests.digest = MatchingTraces.digest`
q := getQuery(ctx)
var triageStatuses []schema.ExpectationLabel
@@ -560,12 +558,7 @@
if q.IncludePositiveDigests {
triageStatuses = append(triageStatuses, schema.LabelPositive)
}
-
- ignoreStatuses := []bool{false}
- if q.IncludeIgnoredTraces {
- ignoreStatuses = append(ignoreStatuses, true)
- }
- arguments := []interface{}{triageStatuses, getFirstCommitID(ctx), q.TraceValues[types.CorpusField][0], ignoreStatuses}
+ arguments := append([]interface{}{triageStatuses}, args...)
rows, err := s.db.Query(ctx, statement, arguments...)
if err != nil {
@@ -583,6 +576,86 @@
return rv, nil
}
+type filterSets struct {
+ key string
+ values []string
+}
+
+// matchingTracesStatement returns a SQL snippet that includes a WITH table called MatchingTraces.
+// This table will have rows containing trace_id, grouping_id, and digest of traces that match
+// the given search criteria. The second parameter is the arguments that need to be included
+// in the query. This code knows to start using numbered parameters at 2.
+func matchingTracesStatement(ctx context.Context) (string, []interface{}) {
+ var keyFilters []filterSets
+ q := getQuery(ctx)
+ for key, values := range q.TraceValues {
+ if key == types.CorpusField {
+ continue
+ }
+ if key != sql.Sanitize(key) {
+ sklog.Infof("key %q did not pass sanitization", key)
+ continue
+ }
+ keyFilters = append(keyFilters, filterSets{key: key, values: values})
+ }
+ ignoreStatuses := []bool{false}
+ if q.IncludeIgnoredTraces {
+ ignoreStatuses = append(ignoreStatuses, true)
+ }
+ args := []interface{}{getFirstCommitID(ctx), ignoreStatuses}
+
+ if len(keyFilters) == 0 {
+ // Corpus is being used as a string
+ args = append(args, q.TraceValues[types.CorpusField][0])
+ return `
+MatchingTraces AS (
+ SELECT trace_id, grouping_id, digest FROM ValuesAtHead
+ WHERE most_recent_commit_id >= $2 AND
+ matches_any_ignore_rule = ANY($3) AND
+ corpus = $4
+)`, args
+ }
+ // Corpus is being used as a JSONB value here
+ args = append(args, `"`+q.TraceValues[types.CorpusField][0]+`"`)
+ return joinedTracesStatement(keyFilters) + `
+MatchingTraces AS (
+ SELECT ValuesAtHead.trace_id, grouping_id, digest FROM ValuesAtHead
+ JOIN JoinedTraces ON ValuesAtHead.trace_id = JoinedTraces.trace_id
+ WHERE most_recent_commit_id >= $2 AND
+ matches_any_ignore_rule = ANY($3)
+)`, args
+}
+
+// joinedTracesStatement returns a SQL snippet that includes a WITH table called JoinedTraces.
+// This table contains just the trace_ids that match the given filters. filters is expected to
+// have keys which passed sanitization (it will sanitize the values). The snippet will include
+// other tables that will be unioned and intersected to create the appropriate rows. This is
+// similar to the technique we use for ingore rules, chosen to maximize consistent performance
+// by using the inverted key index. The keys and values are hardcoded into the string instead
+// of being passed in as arguments because kjlubick@ was not able to use the placeholder values
+//to compare JSONB types removed from a JSONB object to a string while still using the indexes.
+func joinedTracesStatement(filters []filterSets) string {
+ statement := ""
+ for i, filter := range filters {
+ statement += fmt.Sprintf("U%d AS (\n", i)
+ for j, value := range filter.values {
+ if j != 0 {
+ statement += "\tUNION\n"
+ }
+ statement += fmt.Sprintf("\tSELECT trace_id FROM Traces WHERE keys -> '%s' = '%q'\n", filter.key, sql.Sanitize(value))
+ }
+ statement += "),\n"
+ }
+ statement += "JoinedTraces AS (\n"
+ for i := range filters {
+ statement += fmt.Sprintf("\tSELECT trace_id FROM U%d\n\tINTERSECT\n", i)
+ }
+ // Include a final intersect for the corpus. The calling logic will make sure a JSONB value
+ // (i.e. a quoted string) is in the arguments slice.
+ statement += "\tSELECT trace_id FROM Traces where keys -> 'source_type' = $4\n),\n"
+ return statement
+}
+
type stageTwoResult struct {
leftDigest schema.DigestBytes
groupingID schema.GroupingID
@@ -743,20 +816,15 @@
func (s *Impl) getParamsetsForDigests(ctx context.Context, inputs []stageTwoResult) (map[types.Digest]paramtools.ParamSet, error) {
ctx, span := trace.StartSpan(ctx, "getParamsetsForDigests")
defer span.End()
- var leftDigests []schema.DigestBytes
var rightDigests []schema.DigestBytes
for _, input := range inputs {
- leftDigests = append(leftDigests, input.leftDigest)
rightDigests = append(rightDigests, input.rightDigests...)
}
- span.AddAttributes(trace.Int64Attribute("num_digests", int64(len(leftDigests)+len(rightDigests))))
- digestToTraces, err := s.getLeftParamsets(ctx, leftDigests)
+ span.AddAttributes(trace.Int64Attribute("num_digests", int64(len(rightDigests))))
+ digestToTraces, err := s.addRightTraces(ctx, rightDigests)
if err != nil {
return nil, skerr.Wrap(err)
}
- if err := s.addRightParamsets(ctx, digestToTraces, rightDigests); err != nil {
- return nil, skerr.Wrap(err)
- }
traceToOptions, err := s.getOptionsForTraces(ctx, digestToTraces)
if err != nil {
return nil, skerr.Wrap(err)
@@ -768,34 +836,23 @@
return rv, nil
}
-// getLeftParamsets finds the traces that draw the given digests. It will respect the ignore rules
-// setting.
-func (s *Impl) getLeftParamsets(ctx context.Context, digests []schema.DigestBytes) (map[types.Digest][]schema.TraceID, error) {
- ctx, span := trace.StartSpan(ctx, "getLeftParamsets")
+// addRightTraces finds the traces that draw the given digests. We do not need to consider the
+// ignore rules or other search constraints because those only apply to the search results
+// (that is, the left side).
+func (s *Impl) addRightTraces(ctx context.Context, digests []schema.DigestBytes) (map[types.Digest][]schema.TraceID, error) {
+ ctx, span := trace.StartSpan(ctx, "addRightTraces")
defer span.End()
- span.AddAttributes(trace.Int64Attribute("num_left_digests", int64(len(digests))))
+ span.AddAttributes(trace.Int64Attribute("num_right_digests", int64(len(digests))))
- const statement = `WITH
-DigestsAndTraces AS (
- SELECT DISTINCT encode(digest, 'hex') as digest, trace_id
- FROM TiledTraceDigests WHERE digest = ANY($1) AND tile_id >= $2
-)
-SELECT DigestsAndTraces.digest, DigestsAndTraces.trace_id
-FROM DigestsAndTraces
-JOIN Traces ON DigestsAndTraces.trace_id = Traces.trace_id
-WHERE matches_any_ignore_rule = ANY($3)
+ const statement = `SELECT DISTINCT encode(digest, 'hex') as digest, trace_id
+FROM TiledTraceDigests WHERE digest = ANY($1) AND tile_id >= $2
`
- q := getQuery(ctx)
- ignoreStatues := []bool{false}
- if q.IncludeIgnoredTraces {
- ignoreStatues = append(ignoreStatues, true)
- }
- rows, err := s.db.Query(ctx, statement, digests, getFirstTileID(ctx), ignoreStatues)
+ rows, err := s.db.Query(ctx, statement, digests, getFirstTileID(ctx))
if err != nil {
return nil, skerr.Wrap(err)
}
- defer rows.Close()
digestToTraces := map[types.Digest][]schema.TraceID{}
+ defer rows.Close()
for rows.Next() {
var digest types.Digest
var traceID schema.TraceID
@@ -807,32 +864,6 @@
return digestToTraces, nil
}
-// addRightParamsets finds the traces that draw the given digests. We do not need to consider the
-// ignore rules because those only apply to the search results (that is, the left side).
-func (s *Impl) addRightParamsets(ctx context.Context, digestToTraces map[types.Digest][]schema.TraceID, digests []schema.DigestBytes) error {
- ctx, span := trace.StartSpan(ctx, "getLeftParamsets")
- defer span.End()
- span.AddAttributes(trace.Int64Attribute("num_right_digests", int64(len(digests))))
-
- const statement = `SELECT DISTINCT encode(digest, 'hex') as digest, trace_id
-FROM TiledTraceDigests WHERE digest = ANY($1) AND tile_id >= $2
-`
- rows, err := s.db.Query(ctx, statement, digests, getFirstTileID(ctx))
- if err != nil {
- return skerr.Wrap(err)
- }
- defer rows.Close()
- for rows.Next() {
- var digest types.Digest
- var traceID schema.TraceID
- if err := rows.Scan(&digest, &traceID); err != nil {
- return skerr.Wrap(err)
- }
- digestToTraces[digest] = append(digestToTraces[digest], traceID)
- }
- return nil
-}
-
// getOptionsForTraces returns the most recent option map for each given trace.
func (s *Impl) getOptionsForTraces(ctx context.Context, digestToTraces map[types.Digest][]schema.TraceID) (map[schema.MD5Hash]paramtools.Params, error) {
ctx, span := trace.StartSpan(ctx, "getOptionsForTraces")
@@ -1005,7 +1036,7 @@
// The first digest in the trace group is this digest.
sr.Status = tg.Digests[0].Status
} else {
- // We assume untriaged if digest is not in the window. This would be a surprise.
+ // We assume untriaged if digest is not in the window.
sr.Status = expectations.Untriaged
}
if len(tg.Traces) > 0 {
@@ -1013,6 +1044,12 @@
// the same grouping, which includes test name.
sr.Test = types.TestName(tg.Traces[0].Params[types.PrimaryKeyField])
}
+ leftPS := paramtools.ParamSet{}
+ for _, tr := range tg.Traces {
+ leftPS.AddParams(tr.Params)
+ }
+ leftPS.Normalize()
+ sr.ParamSet = leftPS
rv[i] = sr
}
return rv, nil
diff --git a/golden/go/search2/search2_test.go b/golden/go/search2/search2_test.go
index 0a71451..d518049 100644
--- a/golden/go/search2/search2_test.go
+++ b/golden/go/search2/search2_test.go
@@ -1158,8 +1158,8 @@
dks.DigestA03Pos: expectations.Positive,
dks.DigestA08Pos: expectations.Positive,
}, dks.TriangleTest: {
- dks.DigestB02Pos: expectations.Positive,
dks.DigestB01Pos: expectations.Positive,
+ dks.DigestB02Pos: expectations.Positive,
},
},
}, res)
@@ -1279,6 +1279,297 @@
}, tg)
}
+func TestSearch_FilterLeftSideByKeys_Success(t *testing.T) {
+ unittest.LargeTest(t)
+
+ ctx := context.Background()
+ db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+ require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, dks.Build()))
+
+ s := New(db, 100)
+ res, err := s.Search(ctx, &query.Search{
+ OnlyIncludeDigestsProducedAtHead: true,
+ IncludePositiveDigests: true,
+ IncludeNegativeDigests: false,
+ IncludeUntriagedDigests: false,
+ Sort: query.SortDescending,
+ IncludeIgnoredTraces: false,
+ TraceValues: paramtools.ParamSet{
+ types.CorpusField: []string{dks.CornersCorpus},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ dks.DeviceKey: []string{dks.WalleyeDevice, dks.QuadroDevice},
+ },
+ RGBAMinFilter: 0,
+ RGBAMaxFilter: 255,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &frontend.SearchResponse{
+ Results: []*frontend.SearchResult{{
+ Digest: dks.DigestB01Pos,
+ Test: dks.TriangleTest,
+ Status: expectations.Positive,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.RGBColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ // Notice these are just the devices from the filters, not all devices that
+ // drew this digest.
+ dks.DeviceKey: []string{dks.QuadroDevice, dks.WalleyeDevice},
+ dks.OSKey: []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ TraceGroup: frontend.TraceGroup{
+ Traces: []frontend.Trace{{
+ ID: "555f149dfe944816076a57c633578dbc",
+ DigestIndices: []int{-1, -1, -1, 0, 0, 0, 0, 0, 0, 0},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.RGBColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.QuadroDevice,
+ dks.OSKey: dks.Windows10dot3OS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }, {
+ ID: "7346d80b7d5d1087fd61ae40098f4277",
+ DigestIndices: []int{1, 0, 0, -1, -1, -1, -1, -1, -1, -1},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.RGBColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.QuadroDevice,
+ dks.OSKey: dks.Windows10dot2OS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }, {
+ ID: "ab734e10b7aed9d06a91f46d14746270",
+ DigestIndices: []int{-1, -1, -1, -1, -1, 0, 0, 0, 0, 0},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.RGBColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.WalleyeDevice,
+ dks.OSKey: dks.AndroidOS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }},
+ Digests: []frontend.DigestStatus{
+ {Digest: dks.DigestB01Pos, Status: expectations.Positive},
+ {Digest: dks.DigestBlank, Status: expectations.Untriaged},
+ },
+ TotalDigests: 2,
+ },
+ RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
+ common.PositiveRef: {
+ CombinedMetric: 1.9362538, QueryMetric: 1.9362538, PixelDiffPercent: 43.75, NumDiffPixels: 28,
+ MaxRGBADiffs: [4]int{11, 5, 42, 0},
+ DimDiffer: false,
+ Digest: dks.DigestB02Pos,
+ Status: expectations.Positive,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.GreyColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ dks.DeviceKey: []string{dks.QuadroDevice, dks.IPadDevice, dks.IPhoneDevice, dks.WalleyeDevice},
+ dks.OSKey: []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS, dks.IOS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ },
+ common.NegativeRef: {
+ CombinedMetric: 2.9445405, QueryMetric: 2.9445405, PixelDiffPercent: 10.9375, NumDiffPixels: 7,
+ MaxRGBADiffs: [4]int{250, 244, 197, 51},
+ DimDiffer: false,
+ Digest: dks.DigestB03Neg,
+ Status: expectations.Negative,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.RGBColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ dks.DeviceKey: []string{dks.IPadDevice, dks.IPhoneDevice},
+ dks.OSKey: []string{dks.IOS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ },
+ },
+ ClosestRef: common.PositiveRef,
+ }, {
+ Digest: dks.DigestB02Pos,
+ Test: dks.TriangleTest,
+ Status: expectations.Positive,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.GreyColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ // Notice these are just the devices from the filters, not all devices that
+ // drew this digest.
+ dks.DeviceKey: []string{dks.QuadroDevice, dks.WalleyeDevice},
+ dks.OSKey: []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ TraceGroup: frontend.TraceGroup{
+ Traces: []frontend.Trace{{
+ ID: "9a42e1337f848e4dbfa9688dda60fe7b",
+ DigestIndices: []int{-1, -1, -1, -1, -1, 0, 0, -1, 0, 0},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.GreyColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.WalleyeDevice,
+ dks.OSKey: dks.AndroidOS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }, {
+ ID: "b9c96f249f2551a5d33f264afdb23a46",
+ DigestIndices: []int{-1, -1, -1, 0, 0, 0, 0, 0, 0, -1},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.GreyColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.QuadroDevice,
+ dks.OSKey: dks.Windows10dot3OS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }, {
+ ID: "c0f2834eb3408acdc799dc5190e3533e",
+ DigestIndices: []int{0, 0, 0, -1, -1, -1, -1, -1, -1, -1},
+ Params: paramtools.Params{
+ dks.ColorModeKey: dks.GreyColorMode,
+ types.CorpusField: dks.CornersCorpus,
+ dks.DeviceKey: dks.QuadroDevice,
+ dks.OSKey: dks.Windows10dot2OS,
+ types.PrimaryKeyField: dks.TriangleTest,
+ "ext": "png",
+ },
+ }},
+ Digests: []frontend.DigestStatus{
+ {Digest: dks.DigestB02Pos, Status: expectations.Positive},
+ },
+ TotalDigests: 1,
+ },
+ RefDiffs: map[common.RefClosest]*frontend.SRDiffDigest{
+ common.PositiveRef: {
+ CombinedMetric: 1.9362538, QueryMetric: 1.9362538, PixelDiffPercent: 43.75, NumDiffPixels: 28,
+ MaxRGBADiffs: [4]int{11, 5, 42, 0},
+ DimDiffer: false,
+ Digest: dks.DigestB01Pos,
+ Status: expectations.Positive,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.RGBColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ dks.DeviceKey: []string{dks.QuadroDevice, dks.IPadDevice, dks.IPhoneDevice, dks.TaimenDevice, dks.WalleyeDevice},
+ dks.OSKey: []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS, dks.IOS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ },
+ common.NegativeRef: {
+ CombinedMetric: 6.489451, QueryMetric: 6.489451, PixelDiffPercent: 53.125, NumDiffPixels: 34,
+ MaxRGBADiffs: [4]int{250, 244, 197, 51},
+ DimDiffer: false,
+ Digest: dks.DigestB03Neg,
+ Status: expectations.Negative,
+ ParamSet: paramtools.ParamSet{
+ dks.ColorModeKey: []string{dks.RGBColorMode},
+ types.CorpusField: []string{dks.CornersCorpus},
+ dks.DeviceKey: []string{dks.IPadDevice, dks.IPhoneDevice},
+ dks.OSKey: []string{dks.IOS},
+ types.PrimaryKeyField: []string{dks.TriangleTest},
+ "ext": []string{"png"},
+ },
+ },
+ },
+ ClosestRef: common.PositiveRef,
+ }},
+ Offset: 0,
+ Size: 2,
+ Commits: kitchenSinkCommits,
+ BulkTriageData: web_frontend.TriageRequestData{
+ dks.TriangleTest: {
+ dks.DigestB01Pos: expectations.Positive,
+ dks.DigestB02Pos: expectations.Positive,
+ },
+ },
+ }, res)
+}
+
+func TestJoinedTracesStatement_Success(t *testing.T) {
+ unittest.SmallTest(t)
+
+ statement := joinedTracesStatement([]filterSets{
+ {key: "key1", values: []string{"alpha", "beta"}},
+ })
+ expectedCondition := `U0 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"alpha"'
+ UNION
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"beta"'
+),
+JoinedTraces AS (
+ SELECT trace_id FROM U0
+ INTERSECT
+ SELECT trace_id FROM Traces where keys -> 'source_type' = $4
+),
+`
+ assert.Equal(t, expectedCondition, statement)
+
+ statement = joinedTracesStatement([]filterSets{
+ {key: "key1", values: []string{"alpha", "beta"}},
+ {key: "key2", values: []string{"gamma"}},
+ {key: "key3", values: []string{"delta", "epsilon", "zeta"}},
+ })
+ expectedCondition = `U0 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"alpha"'
+ UNION
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"beta"'
+),
+U1 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key2' = '"gamma"'
+),
+U2 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key3' = '"delta"'
+ UNION
+ SELECT trace_id FROM Traces WHERE keys -> 'key3' = '"epsilon"'
+ UNION
+ SELECT trace_id FROM Traces WHERE keys -> 'key3' = '"zeta"'
+),
+JoinedTraces AS (
+ SELECT trace_id FROM U0
+ INTERSECT
+ SELECT trace_id FROM U1
+ INTERSECT
+ SELECT trace_id FROM U2
+ INTERSECT
+ SELECT trace_id FROM Traces where keys -> 'source_type' = $4
+),
+`
+ assert.Equal(t, expectedCondition, statement)
+}
+
+func TestJoinedTracesStatement_RemovesBadSQLCharacters(t *testing.T) {
+ unittest.SmallTest(t)
+
+ statement := joinedTracesStatement([]filterSets{
+ {key: "key1", values: []string{"alpha", `beta"' OR 1=1`}},
+ {key: `key2'='""' OR 1=1`, values: []string{"1"}}, // invalid keys are removed entirely.
+ })
+ expectedCondition := `U0 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"alpha"'
+ UNION
+ SELECT trace_id FROM Traces WHERE keys -> 'key1' = '"beta OR 1=1"'
+),
+U1 AS (
+ SELECT trace_id FROM Traces WHERE keys -> 'key2'='""' OR 1=1' = '"1"'
+),
+JoinedTraces AS (
+ SELECT trace_id FROM U0
+ INTERSECT
+ SELECT trace_id FROM U1
+ INTERSECT
+ SELECT trace_id FROM Traces where keys -> 'source_type' = $4
+),
+`
+ assert.Equal(t, expectedCondition, statement)
+}
+
var kitchenSinkCommits = makeKitchenSinkCommits()
func makeKitchenSinkCommits() []web_frontend.Commit {
diff --git a/golden/go/sql/util.go b/golden/go/sql/util.go
index 9d447f1..eeaf235 100644
--- a/golden/go/sql/util.go
+++ b/golden/go/sql/util.go
@@ -118,3 +118,9 @@
}
return pieces[1]
}
+
+// Sanitize removes unsafe characters from strings to avoid SQL injections. Namely quotes.
+func Sanitize(s string) string {
+ s = strings.ReplaceAll(s, `'`, ``)
+ return strings.ReplaceAll(s, `"`, ``)
+}
diff --git a/golden/go/sql/util_test.go b/golden/go/sql/util_test.go
index 7afa9e9..220438a 100644
--- a/golden/go/sql/util_test.go
+++ b/golden/go/sql/util_test.go
@@ -150,3 +150,11 @@
assert.Equal(t, "1234_6789012", Unqualify("gerrit_1234_6789012"))
assert.Equal(t, "67890", Unqualify("67890"))
}
+
+func TestSanitize_Success(t *testing.T) {
+ unittest.SmallTest(t)
+
+ assert.Equal(t, "All Good!", Sanitize(`All Good!`))
+ assert.Equal(t, "foo OR 1=1", Sanitize(`foo OR '1=1'`))
+ assert.Equal(t, "foo OR 1=1", Sanitize(`foo OR "1=1"`))
+}