storage: optimize queries for label presence

The query term "key>" can be used to search for records that have any
value for the key. This optimizes the query with a separate SQL
expression and adds tests for this behavior.

Change-Id: I5e2734734e0911a3ed12c87e7ada776ad309a90d
Reviewed-on: https://go-review.googlesource.com/36590
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/db/db.go b/storage/db/db.go
index 5c03b4f..38d5011 100644
--- a/storage/db/db.go
+++ b/storage/db/db.go
@@ -377,6 +377,10 @@
 		}
 		return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value = ?", []interface{}{key, value}, nil
 	case '>', '<':
+		if sep == '>' && value == "" {
+			// Simplify queries for any value.
+			return "SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ?", []interface{}{key}, nil
+		}
 		return fmt.Sprintf("SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value %c ?", sep), []interface{}{key, value}, nil
 	}
 	return "", nil, fmt.Errorf("query part %q has invalid key", part)
diff --git a/storage/db/db_test.go b/storage/db/db_test.go
index b716623..48a257e 100644
--- a/storage/db/db_test.go
+++ b/storage/db/db_test.go
@@ -12,6 +12,7 @@
 	"os"
 	"os/exec"
 	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 	"testing"
@@ -273,8 +274,10 @@
 		{"label0:0 label0:5", []int{}},
 		{"bogus query", nil},
 		{"label1<2 label3:0", []int{0, 1, 2, 3}},
-		{"label1>510", []int{1022, 1023}},
+		{"label1>510 label1<52", []int{1022, 1023}},
 		{"", allRecords},
+		{"missing>", []int{}},
+		{"label0>", allRecords},
 	}
 	for _, test := range tests {
 		t.Run("query="+test.q, func(t *testing.T) {
@@ -293,21 +296,35 @@
 					t.Errorf("Close: %v", err)
 				}
 			}()
-			for i, num := range test.want {
+			var have []int
+			for i := range test.want {
 				if !q.Next() {
 					t.Fatalf("#%d: Next() = false", i)
 				}
 				r := q.Result()
-				if r.Labels["label0"] != fmt.Sprintf("%d", num) {
-					t.Errorf("result[%d].label0 = %q, want %d", i, r.Labels["label0"], num)
+				n, err := strconv.Atoi(r.Labels["label0"])
+				if err != nil {
+					t.Fatalf("unexpected label0 value %q: %v", r.Labels["label0"], err)
 				}
+				have = append(have, n)
 				if r.NameLabels["name"] != "Name" {
 					t.Errorf("result[%d].name = %q, want %q", i, r.NameLabels["name"], "Name")
 				}
 			}
+			for q.Next() {
+				r := q.Result()
+				t.Errorf("Next() = true, want false (got labels %v)", r.Labels)
+			}
 			if err := q.Err(); err != nil {
 				t.Errorf("Err() = %v, want nil", err)
 			}
+			sort.Ints(have)
+			if len(have) == 0 {
+				have = []int{}
+			}
+			if !reflect.DeepEqual(have, test.want) {
+				t.Errorf("label0[] = %v, want %v", have, test.want)
+			}
 		})
 	}
 }