storage/db: implement range queries

Change-Id: I954c533190353a16be9cbe55541fef7b0aaa818b
Reviewed-on: https://go-review.googlesource.com/35877
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/db/db.go b/storage/db/db.go
index 35b696f..c0aef58 100644
--- a/storage/db/db.go
+++ b/storage/db/db.go
@@ -9,7 +9,6 @@
 import (
 	"bytes"
 	"database/sql"
-	"errors"
 	"fmt"
 	"io"
 	"regexp"
@@ -369,37 +368,44 @@
 	qparts := splitQueryWords(q)
 
 	var args []interface{}
-Words:
-	for _, part := range qparts {
-		for i, c := range part {
-			switch {
-			case c == ':':
-				args = append(args, part[:i], part[i+1:])
-				continue Words
-			case c == '>' || c == '<':
-				// TODO
-				return &Query{err: errors.New("unsupported operator")}
-			case unicode.IsSpace(c) || unicode.IsUpper(c):
-				return &Query{err: fmt.Errorf("query part %q has invalid key", part)}
-			}
-		}
-		return &Query{err: fmt.Errorf("query part %q is missing operator", part)}
-	}
-
 	query := "SELECT r.Content FROM "
-	for i := 0; i < len(args)/2; i++ {
+	for i, part := range qparts {
 		if i > 0 {
 			query += " INNER JOIN "
 		}
-		query += fmt.Sprintf("(SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value = ?) t%d", i)
+		sepIndex := strings.IndexFunc(part, func(r rune) bool {
+			return r == ':' || r == '>' || r == '<' || unicode.IsSpace(r) || unicode.IsUpper(r)
+		})
+		if sepIndex < 0 {
+			return &Query{err: fmt.Errorf("query part %q is missing operator", part)}
+		}
+		key, sep, value := part[:sepIndex], part[sepIndex], part[sepIndex+1:]
+		switch sep {
+		case ':':
+			if value == "" {
+				// TODO(quentin): Implement support for searching for missing labels.
+				return &Query{err: fmt.Errorf("missing value for query part %q", part)}
+			}
+			query += fmt.Sprintf("(SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value = ?) t%d", i)
+			args = append(args, key, value)
+		case '>', '<':
+			query += fmt.Sprintf("(SELECT UploadID, RecordID FROM RecordLabels WHERE Name = ? AND Value %c ?) t%d", sep, i)
+			args = append(args, key, value)
+		default:
+			return &Query{err: fmt.Errorf("query part %q has invalid key", part)}
+		}
 		if i > 0 {
 			query += " USING (UploadID, RecordID)"
 		}
 	}
 
-	// TODO(quentin): Handle empty query string.
-
-	query += " LEFT JOIN Records r USING (UploadID, RecordID)"
+	if len(qparts) > 0 {
+		query += " LEFT JOIN"
+	}
+	query += " Records r"
+	if len(qparts) > 0 {
+		query += " USING (UploadID, RecordID)"
+	}
 
 	rows, err := db.sql.Query(query, args...)
 	if err != nil {
diff --git a/storage/db/db_test.go b/storage/db/db_test.go
index 154f4b1..046622e 100644
--- a/storage/db/db_test.go
+++ b/storage/db/db_test.go
@@ -263,7 +263,10 @@
 		t.Fatalf("NewUpload: %v", err)
 	}
 
+	var allRecords []int
+
 	for i := 0; i < 1024; i++ {
+		allRecords = append(allRecords, i)
 		r := &benchfmt.Result{Labels: make(map[string]string), NameLabels: make(map[string]string), Content: "BenchmarkName 1 ns/op"}
 		for j := uint(0); j < 10; j++ {
 			r.Labels[fmt.Sprintf("label%d", j)] = fmt.Sprintf("%d", i/(1<<j))
@@ -286,6 +289,9 @@
 		{"label0:5 name:Name", []int{5}},
 		{"label0:0 label0:5", []int{}},
 		{"bogus query", nil},
+		{"label1<2 label3:0", []int{0, 1, 2, 3}},
+		{"label1>510", []int{1022, 1023}},
+		{"", allRecords},
 	}
 	for _, test := range tests {
 		t.Run("query="+test.q, func(t *testing.T) {