blob: 0041531489a9133396593bbf119b847bd499c07c [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"github.com/Masterminds/squirrel"
"github.com/lib/pq"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/database"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware/stats"
"golang.org/x/pkgsite/internal/version"
"golang.org/x/sync/errgroup"
)
// GetVersionsForPath returns a list of tagged versions sorted in
// descending semver order if any exist. If none, it returns the 10 most
// recent from a list of pseudo-versions sorted in descending semver order.
func (db *DB) GetVersionsForPath(ctx context.Context, path string) (_ []*internal.ModuleInfo, err error) {
defer derrors.WrapStack(&err, "GetVersionsForPath(ctx, %q)", path)
defer stats.Elapsed(ctx, "GetVersionsForPath")()
versions, err := getPathVersions(ctx, db, path, version.TypeRelease, version.TypePrerelease)
if err != nil {
return nil, err
}
if len(versions) != 0 {
return versions, nil
}
versions, err = getPathVersions(ctx, db, path, version.TypePseudo)
if err != nil {
return nil, err
}
return versions, nil
}
// getPathVersions returns a list of versions sorted in descending semver
// order. The version types included in the list are specified by a list of
// VersionTypes.
func getPathVersions(ctx context.Context, db *DB, path string, versionTypes ...version.Type) (_ []*internal.ModuleInfo, err error) {
defer derrors.WrapStack(&err, "getPathVersions(ctx, db, %q, %v)", path, versionTypes)
baseQuery := `
SELECT
m.module_path,
m.version,
m.commit_time,
m.redistributable,
m.has_go_mod,
m.source_info
FROM modules m
INNER JOIN units u
ON u.module_id = m.id
WHERE
u.v1path_id = (
SELECT u2.v1path_id
FROM units as u2
INNER JOIN paths p
ON p.id = u2.path_id
WHERE p.path = $1
LIMIT 1
)
AND version_type in (%s)
ORDER BY
m.incompatible,
m.module_path DESC,
m.sort_version DESC %s`
queryEnd := `;`
if len(versionTypes) == 0 {
return nil, fmt.Errorf("error: must specify at least one version type")
} else if len(versionTypes) == 1 && versionTypes[0] == version.TypePseudo {
queryEnd = `LIMIT 10;`
} else {
// When a page shows too many versions, it can result in a Chrome CSS
// bug: https://bugs.chromium.org/p/chromium/issues/detail?id=688640.
// For example,
// https://pkg.go.dev/github.com/aws/aws-sdk-go/aws/signer/v4?tab=versions.
// It's not that useful to see that many versions on a page anyway, so
// just limit to 800 versions.
queryEnd = `LIMIT 800;`
}
query := fmt.Sprintf(baseQuery, versionTypeExpr(versionTypes), queryEnd)
var versions []*internal.ModuleInfo
collect := func(rows *sql.Rows) error {
mi, err := scanModuleInfo(rows.Scan)
if err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
versions = append(versions, mi)
return nil
}
if err := db.db.RunQuery(ctx, query, collect, path); err != nil {
return nil, err
}
if err := populateLatestInfos(ctx, db, versions); err != nil {
return nil, err
}
return versions, nil
}
// versionTypeExpr returns a comma-separated list of version types,
// for use in a clause like "WHERE version_type IN (%s)"
func versionTypeExpr(vts []version.Type) string {
var vs []string
for _, vt := range vts {
vs = append(vs, fmt.Sprintf("'%s'", vt.String()))
}
return strings.Join(vs, ", ")
}
func populateLatestInfo(ctx context.Context, db *DB, mi *internal.ModuleInfo) (err error) {
defer derrors.WrapStack(&err, "populateLatestInfo(%q)", mi.ModulePath)
// Get information about retractions an deprecations, and apply it.
lmv, err := db.GetLatestModuleVersions(ctx, mi.ModulePath)
if err != nil {
return err
}
if lmv != nil {
lmv.PopulateModuleInfo(mi)
}
return nil
}
func populateLatestInfos(ctx context.Context, db *DB, mis []*internal.ModuleInfo) (err error) {
defer derrors.WrapStack(&err, "populateLatestInfos(%d ModuleInfos)", len(mis))
// Collect the LatestModuleVersions for all modules in the list.
lmvs := map[string]*internal.LatestModuleVersions{}
for _, mi := range mis {
if _, ok := lmvs[mi.ModulePath]; !ok {
lmv, err := db.GetLatestModuleVersions(ctx, mi.ModulePath)
if err != nil {
return err
}
lmvs[mi.ModulePath] = lmv
}
}
// Use the collected LatestModuleVersions to populate the ModuleInfos.
for _, mi := range mis {
lmv := lmvs[mi.ModulePath]
if lmv != nil {
lmv.PopulateModuleInfo(mi)
}
}
return nil
}
// GetLatestInfo returns the latest information about the unit in the module.
// See internal.LatestInfo for documentation about the returned values.
// If latestUnitMeta is non-nil, it is the result of GetUnitMeta(unitPath, internal.UnknownModulePath, internal.LatestVersion).
// That can save a redundant call to GetUnitMeta here.
func (db *DB) GetLatestInfo(ctx context.Context, unitPath, modulePath string, latestUnitMeta *internal.UnitMeta) (latest internal.LatestInfo, err error) {
defer derrors.WrapStack(&err, "DB.GetLatestInfo(ctx, %q, %q)", unitPath, modulePath)
defer stats.Elapsed(ctx, "DB.GetLatestInfo")()
group, gctx := errgroup.WithContext(ctx)
if latestUnitMeta != nil {
latest.MinorVersion = latestUnitMeta.Version
latest.MinorModulePath = latestUnitMeta.ModulePath
} else {
group.Go(func() error {
um, err := db.GetUnitMeta(gctx, unitPath, internal.UnknownModulePath, version.Latest)
if err != nil {
return err
}
latest.MinorVersion = um.Version
latest.MinorModulePath = um.ModulePath
return nil
})
}
group.Go(func() (err error) {
latest.MajorModulePath, latest.MajorUnitPath, err = db.getLatestMajorVersion(gctx, unitPath, modulePath)
return err
})
group.Go(func() (err error) {
latest.UnitExistsAtMinor, err = db.unitExistsAtLatest(gctx, unitPath, modulePath)
return err
})
if err := group.Wait(); err != nil {
return internal.LatestInfo{}, err
}
return latest, nil
}
// getLatestMajorVersion returns the latest module path and the full package path
// of the latest version found, given the fullPath and the modulePath.
// For example, in the module path "github.com/casbin/casbin", there
// is another module path with a greater major version "github.com/casbin/casbin/v3".
// This function will return "github.com/casbin/casbin/v3" or the input module path
// if no later module path was found. It also returns the full package path at the
// latest module version if it exists. If not, it returns the module path.
//
// getLatestMajorVersion only considers tagged (non-pseudo) versions. If there are none,
// it returns empty strings.
func (db *DB) getLatestMajorVersion(ctx context.Context, fullPath, modulePath string) (modPath, pkgPath string, err error) {
defer derrors.WrapStack(&err, "DB.getLatestMajorVersion2(%q)", modulePath)
defer stats.Elapsed(ctx, "DB.getLatestMajorVersion")()
// Collect all the non-deprecated module paths for the series that have at
// least one good version, along with that good version. A good version
// is both servable (in the modules table) and not retracted.
seriesPath := internal.SeriesPathForModule(modulePath)
q, args, err := squirrel.Select("p.path", "l.good_version").
From("latest_module_versions l").
Join("paths p ON p.id = l.module_path_id").
Where(squirrel.Eq{"l.series_path": seriesPath}).
Where("NOT l.deprecated").
Where(squirrel.NotEq{"l.good_version": ""}).
PlaceholderFormat(squirrel.Dollar).
ToSql()
if err != nil {
return "", "", err
}
type pathver struct {
path, version string
}
var pathvers []pathver
err = db.db.RunQuery(ctx, q, func(rows *sql.Rows) error {
var pv pathver
if err := rows.Scan(&pv.path, &pv.version); err != nil {
return err
}
pathvers = append(pathvers, pv)
return nil
}, args...)
if err != nil {
return "", "", err
}
// Find the highest tagged version from among the (module path, good
// version) pairs.
var max pathver
for _, pv := range pathvers {
if version.IsPseudo(pv.version) {
continue
}
// Use semver.Compare, not version.Later, because we don't want to prefer
// release to prerelease: we want v2.0.0-pre over v1.0.0.
if max.path == "" || semver.Compare(pv.version, max.version) > 0 {
max = pv
}
}
// No highest tagged version: return empty strings.
if max.path == "" {
return "", "", nil
}
// Find the unit path at the max-version module path.
v1Path := internal.V1Path(fullPath, modulePath)
row := db.db.QueryRow(ctx, `
SELECT p.path
FROM units u
INNER JOIN modules m ON m.id = u.module_id
INNER JOIN paths p ON p.id = u.path_id
INNER JOIN paths p2 ON p2.id = u.v1path_id
WHERE p2.path = $1 AND m.module_path = $2 AND m.version = $3`,
v1Path, max.path, max.version)
var path string
switch err := row.Scan(&path); err {
case nil:
return max.path, path, nil
case sql.ErrNoRows:
return max.path, max.path, nil
default:
return "", "", err
}
}
// unitExistsAtLatest reports whether unitPath exists at the latest version of modulePath.
func (db *DB) unitExistsAtLatest(ctx context.Context, unitPath, modulePath string) (unitExists bool, err error) {
defer derrors.WrapStack(&err, "DB.unitExistsAtLatest(ctx, %q, %q)", unitPath, modulePath)
defer stats.Elapsed(ctx, "DB.unitExistsAtLatest")()
// Find the latest version of the module path in the modules table.
var latestGoodVersion string
lmv, err := db.GetLatestModuleVersions(ctx, modulePath)
if err != nil {
return false, err
}
if lmv != nil && lmv.GoodVersion != "" {
// If we have latest-version info, use it.
latestGoodVersion = lmv.GoodVersion
} else {
// Otherwise, query the modules table, ignoring all adjustments for incompatible and retracted versions.
err := db.db.QueryRow(ctx, `
SELECT version
FROM modules
WHERE module_path = $1
ORDER BY
version_type = 'release' DESC,
sort_version DESC
LIMIT 1
`, modulePath).Scan(&latestGoodVersion)
if err != nil {
return false, err
}
}
if latestGoodVersion == "" {
return true, nil
}
// See if the unit path exists at that version.
var x int
err = db.db.QueryRow(ctx, `
SELECT 1
FROM units u
INNER JOIN paths p ON p.id = u.path_id
INNER JOIN modules m ON m.id = u.module_id
WHERE p.path = $1 AND m.module_path = $2 AND m.version = $3
`, unitPath, modulePath, latestGoodVersion).Scan(&x)
switch err {
case nil:
return true, nil
case sql.ErrNoRows:
return false, nil
default:
return false, err
}
}
func (db *DB) getMultiLatestModuleVersions(ctx context.Context, modulePaths []string) (lmvs []*internal.LatestModuleVersions, err error) {
defer derrors.WrapStack(&err, "getMultiLatestModuleVersions(%v)", modulePaths)
defer stats.Elapsed(ctx, "getMultiLatestModuleVersions")()
collect := func(rows *sql.Rows) error {
var (
modulePath, raw, cooked, good string
goModBytes []byte
)
if err := rows.Scan(&modulePath, &raw, &cooked, &good, &goModBytes); err != nil {
return err
}
lmv, err := internal.NewLatestModuleVersions(modulePath, raw, cooked, good, goModBytes)
if err != nil {
return err
}
lmvs = append(lmvs, lmv)
return nil
}
err = db.db.RunQuery(ctx, `
SELECT p.path, r.raw_version, r.cooked_version, r.good_version, r.raw_go_mod_bytes
FROM latest_module_versions r
INNER JOIN paths p ON p.id = r.module_path_id
WHERE p.path = ANY($1)
AND r.status = 200
ORDER BY p.path DESC
`, collect, pq.Array(modulePaths))
if err != nil {
return nil, err
}
return lmvs, nil
}
// getLatestGoodVersion returns the latest version of a module in the modules
// table, respecting the retractions and other information in the given
// LatestModuleVersions. If lmv is nil, it finds the latest version, favoring
// release over pre-release, including incompatible versions, and ignoring
// retractions.
func getLatestGoodVersion(ctx context.Context, tx *database.DB, modulePath string, lmv *internal.LatestModuleVersions) (_ string, err error) {
defer derrors.WrapStack(&err, "getLatestGoodVersion(%q)", modulePath)
// Read the versions from the modules table.
// If the cooked latest version is incompatible, then include
// incompatible versions. If it isn't, then either there are no
// incompatible versions, or there are but the latest compatible version
// has a go.mod file. Either way, ignore incompatible versions.
q := squirrel.Select("version").
From("modules").
Where(squirrel.Eq{"module_path": modulePath}).
PlaceholderFormat(squirrel.Dollar)
if lmv != nil && !version.IsIncompatible(lmv.CookedVersion) {
q = q.Where("NOT incompatible")
}
query, args, err := q.ToSql()
if err != nil {
return "", err
}
vs, err := database.Collect1[string](ctx, tx, query, args...)
if err != nil {
return "", err
}
// Choose the latest good version from a filtered list of versions.
if lmv != nil {
// Remove retracted versions.
vs = version.RemoveIf(vs, lmv.IsRetracted)
// The good version should never be later than the cooked version.
if lmv.CookedVersion != "" {
vs = version.RemoveIf(vs, func(v string) bool {
return version.Later(v, lmv.CookedVersion)
})
}
}
return version.LatestOf(vs), nil
}
// GetLatestModuleVersions returns the row of the latest_module_versions table for modulePath.
// If the module path is not found, it returns nil, nil.
func (db *DB) GetLatestModuleVersions(ctx context.Context, modulePath string) (_ *internal.LatestModuleVersions, err error) {
lmv, _, err := getLatestModuleVersions(ctx, db.db, modulePath)
return lmv, err
}
func getLatestModuleVersions(ctx context.Context, db *database.DB, modulePath string) (_ *internal.LatestModuleVersions, id int, err error) {
derrors.WrapStack(&err, "getLatestModuleVersions(%q)", modulePath)
var (
raw, cooked, good string
goModBytes []byte
status int
)
err = db.QueryRow(ctx, `
SELECT
r.module_path_id, r.raw_version, r.cooked_version, r.good_version, r.raw_go_mod_bytes, r.status
FROM latest_module_versions r
INNER JOIN paths p ON p.id = r.module_path_id
WHERE p.path = $1`,
modulePath).Scan(&id, &raw, &cooked, &good, &goModBytes, &status)
if err != nil {
if err == sql.ErrNoRows {
return nil, 0, nil
}
return nil, 0, err
}
if status != 200 {
// No information for this module path, but the ID is still useful.
return nil, id, nil
}
lmv, err := internal.NewLatestModuleVersions(modulePath, raw, cooked, good, goModBytes)
if err != nil {
return nil, 0, err
}
return lmv, id, nil
}
// rawIsMoreRecent reports whether raw version v1 is more recent than v2.
// v1 is more recent if it is later according to the go command (higher semver,
// preferring release to prerelease). However, the raw latest version can go
// backwards if it was an incompatible version, but then a compatible version
// with a go.mod file is published. For example, the module starts with a
// v2.0.0+incompatible, but then the author adds a v1.0.0 with a go.mod file,
// making v1.0.0 the new latest.
func rawIsMoreRecent(v1, v2 string) bool {
return version.Later(v1, v2) || (version.IsIncompatible(v2) && !version.IsIncompatible(v1))
}
// UpdateLatestModuleVersions upserts its argument into the latest_module_versions table
// if the row doesn't exist, or the new version is later.
// It returns the version that is in the DB when it completes.
func (db *DB) UpdateLatestModuleVersions(ctx context.Context, vNew *internal.LatestModuleVersions) (_ *internal.LatestModuleVersions, err error) {
defer derrors.WrapStack(&err, "UpdateLatestModuleVersions(%q)", vNew.ModulePath)
var vResult *internal.LatestModuleVersions
// We need RepeatableRead here because the INSERT...ON CONFLICT does a read.
err = db.db.Transact(ctx, sql.LevelRepeatableRead, func(tx *database.DB) error {
vCur, id, err := getLatestModuleVersions(ctx, tx, vNew.ModulePath)
if err != nil {
return err
}
// Is vNew the most recent information, or does the DB already have
//something more up to date?
update := vCur == nil || rawIsMoreRecent(vNew.RawVersion, vCur.RawVersion) ||
// If new versions are added, cooked can change even if raw is the same.
(vNew.RawVersion == vCur.RawVersion && vNew.CookedVersion != vCur.CookedVersion)
if !update {
log.Debugf(ctx, "%s: not updating latest module versions", vNew.ModulePath)
vResult = vCur
return nil
}
if vCur == nil {
log.Debugf(ctx, "%s: inserting latest_module_versions raw=%q, cooked=%q",
vNew.ModulePath, vNew.RawVersion, vNew.CookedVersion)
} else {
log.Debugf(ctx, "%s: updating latest_module_versions raw=%q, cooked=%q to raw=%q, cooked=%q",
vNew.ModulePath, vCur.RawVersion, vCur.CookedVersion,
vNew.RawVersion, vNew.CookedVersion)
// If the latest good version is now retracted, recompute it.
if vCur.GoodVersion != "" && vNew.IsRetracted(vCur.GoodVersion) {
good, err := getLatestGoodVersion(ctx, tx, vNew.ModulePath, vNew)
if err != nil {
return err
}
vNew.GoodVersion = good
log.Debugf(ctx, "%s: updating latest_module_versions good=%q", vNew.ModulePath, vNew.GoodVersion)
} else {
vNew.GoodVersion = vCur.GoodVersion
}
}
vResult = vNew
return upsertLatestModuleVersions(ctx, tx, vNew.ModulePath, id, vNew, 200)
})
if err != nil {
return nil, err
}
return vResult, nil
}
// UpdateLatestModuleVersionsStatus updates or inserts a failure status into the
// latest_module_versions table.
// It only updates the table if it doesn't have valid information for the module path.
func (db *DB) UpdateLatestModuleVersionsStatus(ctx context.Context, modulePath string, newStatus int) (err error) {
defer derrors.WrapStack(&err, "UpdateLatestModuleVersionsStatus(%q, %d)", modulePath, newStatus)
// We need RepeatableRead here because the INSERT...ON CONFLICT does a read.
return db.db.Transact(ctx, sql.LevelRepeatableRead, func(tx *database.DB) error {
var id, curStatus int
err := tx.QueryRow(ctx, `
SELECT r.module_path_id, r.status
FROM latest_module_versions r
INNER JOIN paths p ON p.id = r.module_path_id
WHERE p.path = $1`,
modulePath).Scan(&id, &curStatus)
if err != nil && err != sql.ErrNoRows {
return err
}
if curStatus == 200 {
return nil
}
log.Debugf(ctx, "%s: updating latest_module_versions status to %d", modulePath, newStatus)
return upsertLatestModuleVersions(ctx, tx, modulePath, id, nil, newStatus)
})
}
func upsertLatestModuleVersions(ctx context.Context, tx *database.DB, modulePath string, id int, lmv *internal.LatestModuleVersions, status int) (err error) {
defer derrors.WrapStack(&err, "upsertLatestModuleVersions(%s, %d)", modulePath, status)
// If the row doesn't exist, get a path ID for the module path.
if id == 0 {
id, err = upsertPath(ctx, tx, modulePath)
if err != nil {
return err
}
}
var (
raw, cooked, good string
goModBytes = []byte{} // not nil, a zero-length slice
deprecated bool
)
if lmv != nil {
raw = lmv.RawVersion
cooked = lmv.CookedVersion
good = lmv.GoodVersion
deprecated = lmv.Deprecated
// Convert the go.mod file into bytes.
goModBytes, err = lmv.GoModFile.Format()
if err != nil {
return err
}
}
_, err = tx.Exec(ctx, `
INSERT INTO latest_module_versions (
module_path_id,
series_path,
raw_version,
cooked_version,
good_version,
deprecated,
raw_go_mod_bytes,
status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (module_path_id)
DO UPDATE SET
series_path=excluded.series_path,
raw_version=excluded.raw_version,
cooked_version=excluded.cooked_version,
good_version=excluded.good_version,
deprecated=excluded.deprecated,
raw_go_mod_bytes=excluded.raw_go_mod_bytes,
status=excluded.status
`,
id, internal.SeriesPathForModule(modulePath), raw, cooked, good, deprecated, goModBytes, status)
return err
}
// UpdateLatestGoodVersion updates the latest version of modulePath.
func (db *DB) UpdateLatestGoodVersion(ctx context.Context, modulePath string) error {
return db.db.Transact(ctx, sql.LevelRepeatableRead, func(tx *database.DB) error {
latest, err := getLatestGoodVersion(ctx, tx, modulePath, nil)
if err != nil {
return err
}
return updateLatestGoodVersion(ctx, tx, modulePath, latest)
})
}
// updateLatestGoodVersion updates latest_module_versions.good_version for modulePath to version.
func updateLatestGoodVersion(ctx context.Context, tx *database.DB, modulePath, version string) (err error) {
defer derrors.WrapStack(&err, "updateLatestGoodVersion(%q, %q)", modulePath, version)
n, err := tx.Exec(ctx, `
UPDATE latest_module_versions
SET good_version = $2
WHERE module_path_id = (
SELECT id FROM paths
WHERE path = $1
)`, modulePath, version)
if err != nil {
return err
}
switch n {
case 0:
log.Debugf(ctx, "updateLatestGoodVersion(%q, %q): no change", modulePath, version)
case 1:
log.Debugf(ctx, "updateLatestGoodVersion(%q, %q): updated", modulePath, version)
default:
return errors.New("more than one row affected")
}
return nil
}