internal/postgres: change versions sort order

In getPath versions, sort first by version,then by module path.

Module path came first in order to group results by module when a package
appears in more than one module. But it also results in out-of-order
versions, because v11 sorts before v2. Now we only use the module path
to break ties when versions are the same.

Incompatible versions still come last.

Fixes #79632.

Change-Id: I1b66135685a0b36478bb0380da03e515ae64b4cb
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/784460
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
diff --git a/internal/postgres/version.go b/internal/postgres/version.go
index 9dcd664..b6000c2 100644
--- a/internal/postgres/version.go
+++ b/internal/postgres/version.go
@@ -94,11 +94,12 @@
 			LIMIT 1
 		)
 		AND version_type in (%s)
-		AND ($3 = '' OR (NOT m.incompatible, m.module_path, m.sort_version) <= (NOT $2, $3, $4))
+		AND ($3 = '' OR (NOT m.incompatible, m.sort_version, m.module_path) <= (NOT $2, $3, $4))
 	ORDER BY
 		m.incompatible,
-		m.module_path DESC,
-		m.sort_version DESC %s`
+		m.sort_version DESC,
+		m.module_path DESC
+	%s`
 
 	if len(versionTypes) == 0 {
 		return nil, "", fmt.Errorf("error: must specify at least one version type")
@@ -132,7 +133,7 @@
 	// Construct the page token for the next page.
 	// See the comment near the top of this function for the format.
 	if len(versions) > 0 {
-		nextPageToken = makePageToken(lastIncompatible, versions[len(versions)-1].ModulePath, lastSortVersion)
+		nextPageToken = makePageToken(lastIncompatible, lastSortVersion, versions[len(versions)-1].ModulePath)
 	}
 	return versions, nextPageToken, nil
 }
@@ -140,9 +141,10 @@
 // parsePageToken parses a page token for getPathVersions.
 // It return a slice of query args.
 func parsePageToken(s string) (queryArgs []any, err error) {
-	// A page token has the form "I P S"
-	// where I is a bool for incompatible version, P is a module path, and S
-	// is a sort version. Spaces suffice to separate these since none can contain a space.
+	// A page token has the form "I S P"
+	// where I is a bool for incompatible version, S
+	// is a sort version and P is a module path.
+	// Spaces suffice to separate these since none can contain a space.
 	parts := strings.Fields(s)
 	if len(parts) != 3 {
 		return nil, errors.New("invalid page token (wrong # parts)")
@@ -155,8 +157,8 @@
 }
 
 // makePageToken constructs a page token for getPathVersions.
-func makePageToken(inc bool, mpath, version string) string {
-	return fmt.Sprintf("%t %s %s", inc, mpath, version)
+func makePageToken(inc bool, version, mpath string) string {
+	return fmt.Sprintf("%t %s %s", inc, version, mpath)
 }
 
 // versionTypeExpr returns a comma-separated list of version types,
diff --git a/internal/postgres/version_test.go b/internal/postgres/version_test.go
index 68c3343..296d07a 100644
--- a/internal/postgres/version_test.go
+++ b/internal/postgres/version_test.go
@@ -19,7 +19,7 @@
 	"golang.org/x/pkgsite/internal/version"
 )
 
-func TestGetVersions(t *testing.T) {
+func TestGetVersionsForPath(t *testing.T) {
 	t.Parallel()
 	var (
 		taggedAndPseudoModule = "path.to/foo"
@@ -266,6 +266,59 @@
 	}
 }
 
+func TestGetPathVersionsSorting(t *testing.T) {
+	// Validate that getPathVersions sorts by version,
+	// then by module path, with incompatible versions last.
+	testDB, release := acquire(t)
+	defer release()
+	testModules := []*internal.Module{
+		sample.Module("m.com/a", "v1.0.0", "pkg"),
+		sample.Module("m.com/a", "v1.2.3", "pkg"),
+		sample.Module("m.com/a", "v1.2.3-pre", "pkg"),
+		sample.Module("m.com/a", "v2.1.1+incompatible", "pkg"),
+		sample.Module("m.com/a", "v2.2.2+incompatible", "pkg"),
+		sample.Module("m.com/a", "v0.0.0-20200101120012-000000000000", "pkg"),
+		sample.Module("m.com/a/v2", "v2.2.2", "pkg"),
+		sample.Module("m.com/a/v2", "v2.1.1", "pkg"),
+		sample.Module("m.com/a/v11", "v11.0.0", "pkg"),
+		// same package path, different module
+		sample.Module("m.com", "v1.2.3", "a/pkg"),
+		sample.Module("m.com", "v1.3.0", "a/pkg"),
+	}
+	for _, m := range testModules {
+		testDB.MustInsertModule(t, m)
+	}
+	gotmods, _, err := getPathVersions(t.Context(), testDB, "m.com/a", "", 0, version.TypePrerelease, version.TypeRelease, version.TypePseudo)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type modver struct {
+		ModulePath, Version string
+	}
+	want := []modver{
+		{"m.com/a/v11", "v11.0.0"},
+		{"m.com/a/v2", "v2.2.2"},
+		{"m.com/a/v2", "v2.1.1"},
+		{"m.com", "v1.3.0"},
+		// version ties are broken by module path
+		{"m.com/a", "v1.2.3"},
+		{"m.com", "v1.2.3"},
+		{"m.com/a", "v1.2.3-pre"},
+		{"m.com/a", "v1.0.0"},
+		{"m.com/a", "v0.0.0-20200101120012-000000000000"},
+		{"m.com/a", "v2.2.2+incompatible"},
+		{"m.com/a", "v2.1.1+incompatible"},
+	}
+	var got []modver
+	for _, mi := range gotmods {
+		got = append(got, modver{mi.ModulePath, mi.Version})
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("mismatch (-want, +got):\n%s", diff)
+	}
+}
+
 func TestGetLatestInfo(t *testing.T) {
 	t.Parallel()
 	testDB, release := acquire(t)