internal/postgres: add GetLatestMinorModuleVersion

Add a method that returns the latest minor version of a module,
and whether a unit exists in that version.

For golang/go#37631

Change-Id: I3f612b5df561c39831494388bb37bc6ee9096168
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/279460
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/postgres/version.go b/internal/postgres/version.go
index fdf8143..1a9c4c4 100644
--- a/internal/postgres/version.go
+++ b/internal/postgres/version.go
@@ -133,7 +133,7 @@
 	v1Path := internal.V1Path(fullPath, modulePath)
 	row = db.db.QueryRow(ctx, `SELECT path FROM units WHERE v1_path = $1 AND module_id = $2;`, v1Path, modID)
 	var path string
-	switch row.Scan(&path) {
+	switch err := row.Scan(&path); err {
 	case nil:
 		return modPath, path, nil
 	case sql.ErrNoRows:
@@ -142,3 +142,36 @@
 		return "", "", err
 	}
 }
+
+// GetLatestMinorModuleVersion returns the latest minor version of modulePath,
+// and whether unitPath exists at that version.
+func (db *DB) GetLatestMinorModuleVersion(ctx context.Context, unitPath, modulePath string) (version string, unitExists bool, err error) {
+	defer derrors.Wrap(&err, "DB.GetLatestMinorVersion(ctx, %q, %q)", unitPath, modulePath)
+
+	// Find the latest version of the module path.
+	var modID int
+	q, args, err := orderByLatest(squirrel.Select("m.version", "m.id").
+		From("modules m").
+		Where(squirrel.Eq{"m.module_path": modulePath})).
+		Limit(1).
+		ToSql()
+	if err != nil {
+		return "", false, err
+	}
+	row := db.db.QueryRow(ctx, q, args...)
+	if err := row.Scan(&version, &modID); err != nil {
+		return "", false, err
+	}
+
+	// See if the unit path exists at that version.
+	var x int
+	err = db.db.QueryRow(ctx, `SELECT 1 FROM units WHERE path = $1 AND module_id = $2`, unitPath, modID).Scan(&x)
+	switch err {
+	case nil:
+		return version, true, nil
+	case sql.ErrNoRows:
+		return version, false, nil
+	default:
+		return "", false, err
+	}
+}
diff --git a/internal/postgres/version_test.go b/internal/postgres/version_test.go
index 9e22452..d4713a5 100644
--- a/internal/postgres/version_test.go
+++ b/internal/postgres/version_test.go
@@ -298,3 +298,43 @@
 		}
 	}
 }
+
+func TestGetLatestMinorModuleVersion(t *testing.T) {
+	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+	defer cancel()
+	defer ResetTestDB(testDB, t)
+
+	const (
+		modulePath    = "foo.com/M"
+		latestVersion = "v1.2.0"
+	)
+
+	for _, m := range []*internal.Module{
+		sample.Module(modulePath, "v1.1.0", "p1", "p2"),
+		sample.Module(modulePath, latestVersion, "p1"),
+		sample.Module(modulePath+"/v2", "v2.0.5", "p1", "p2"),
+	} {
+		if err := testDB.InsertModule(ctx, m); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for _, test := range []struct {
+		unitSuffix  string
+		wantPresent bool
+	}{
+		{"p1", true},
+		{"p2", false},
+	} {
+		gotVersion, gotPresent, err := testDB.GetLatestMinorModuleVersion(ctx, modulePath+"/"+test.unitSuffix, modulePath)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if gotVersion != latestVersion {
+			t.Errorf("%s: got version %q, want %q", test.unitSuffix, gotVersion, latestVersion)
+		}
+		if gotPresent != test.wantPresent {
+			t.Errorf("%s: got present %t, want %t", test.unitSuffix, gotPresent, test.wantPresent)
+		}
+	}
+}