internal/bigquery: order columns in SchemaVersion

In relational algebra and hence bigquery, the order of columns does not
matter. Hence, two schemas with same columns but different ordering in
code definition should be regarded as same. SchemaVersion did not
account for that before. It does so now.

This problem manifested when patching tables in bq: adding a new field
to the middle of govulncheck.Result would result in appending that
column to schema in bq. The schema equality check would then fail
although it should not.

Change-Id: I1d47f59d1d8a415e2976ebe699c37a7b2180a8f6
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/526556
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/bigquery/bigquery.go b/internal/bigquery/bigquery.go
index 59d375a..7f04c37 100644
--- a/internal/bigquery/bigquery.go
+++ b/internal/bigquery/bigquery.go
@@ -12,6 +12,7 @@
 	"errors"
 	"fmt"
 	"net/http"
+	"sort"
 	"strings"
 	"sync"
 	"time"
@@ -295,7 +296,18 @@
 // SchemaString returns a long, human-readable string summarizing schema.
 func SchemaString(schema bq.Schema) string {
 	var b strings.Builder
-	for i, field := range schema {
+
+	// Order of columns does not matter in relational algebra,
+	// so we sort them by column name.
+	var fields []*bq.FieldSchema
+	for _, f := range schema {
+		fields = append(fields, f)
+	}
+	sort.SliceStable(fields, func(i, j int) bool {
+		return fields[i].Name < fields[j].Name // fields cannot have the same name
+	})
+
+	for i, field := range fields {
 		if i > 0 {
 			b.WriteRune(';')
 		}
diff --git a/internal/bigquery/bigquery_test.go b/internal/bigquery/bigquery_test.go
index d3e846e..8753e70 100644
--- a/internal/bigquery/bigquery_test.go
+++ b/internal/bigquery/bigquery_test.go
@@ -70,3 +70,43 @@
 		}
 	}
 }
+
+func TestSchemaString(t *testing.T) {
+	type nest struct {
+		N []byte
+		M float64
+	}
+
+	type s struct {
+		A string
+		B int
+		C []bool
+		D nest
+	}
+	const want = "A,req:STRING;B,req:INTEGER;C,rep:BOOLEAN;D,req:(M,req:FLOAT;N,req:BYTES)"
+	schema, err := InferSchema(s{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	got := SchemaString(schema)
+	if got != want {
+		t.Errorf("\ngot  %q\nwant %q", got, want)
+	}
+
+	// The order of fields should not matter
+	type p struct {
+		A string
+		D nest
+		C []bool
+		B int
+	}
+
+	schema, err = InferSchema(p{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	got = SchemaString(schema)
+	if got != want {
+		t.Errorf("\ngot  %q\nwant %q", got, want)
+	}
+}
diff --git a/internal/govulncheck/govulncheck_test.go b/internal/govulncheck/govulncheck_test.go
index 7b767ca..00519d5 100644
--- a/internal/govulncheck/govulncheck_test.go
+++ b/internal/govulncheck/govulncheck_test.go
@@ -84,29 +84,6 @@
 	}
 }
 
-func TestSchemaString(t *testing.T) {
-	type nest struct {
-		N []byte
-		M float64
-	}
-
-	type s struct {
-		A string
-		B int
-		C []bool
-		D nest
-	}
-	const want = "A,req:STRING;B,req:INTEGER;C,rep:BOOLEAN;D,req:(N,req:BYTES;M,req:FLOAT)"
-	schema, err := bigquery.InferSchema(s{})
-	if err != nil {
-		t.Fatal(err)
-	}
-	got := bigquery.SchemaString(schema)
-	if got != want {
-		t.Errorf("\ngot  %q\nwant %q", got, want)
-	}
-}
-
 func TestIntegration(t *testing.T) {
 	test.NeedsIntegrationEnv(t)