| // 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" | |
| "strconv" | |
| "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. | |
| // TODO: move this and its test to internal/frontend, the only place it's used. | |
| func (db *DB) GetVersionsForPath(ctx context.Context, path string) (_ []*internal.ModuleInfo, err error) { | |
| defer derrors.WrapStack(&err, "getVersionsForPath(ctx, %q)", path) | |
| // 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. | |
| versions, _, err := db.GetPathVersions(ctx, path, "", 800, version.TypeRelease, version.TypePrerelease) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(versions) != 0 { | |
| return versions, nil | |
| } | |
| versions, _, err = db.GetPathVersions(ctx, path, "", 10, 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. | |
| // The result can be paginated by passing pageToken, which should be either the empty | |
| // string or a value returned from a previous call. | |
| // Each subsequent page will begin with the last value of the previous page. | |
| func (db *DB) GetPathVersions(ctx context.Context, path string, startPageToken string, limit int, versionTypes ...version.Type) (_ []*internal.ModuleInfo, nextPageToken string, err error) { | |
| defer derrors.WrapStack(&err, "DB.GetPathVersions(ctx, db, %q, %q, %d, %v)", path, startPageToken, limit, versionTypes) | |
| defer stats.Elapsed(ctx, "GetPathVersions")() | |
| // Get previous values from pageToken. | |
| var pageTokenArgs = []any{false, "", ""} | |
| if startPageToken != "" { | |
| pageTokenArgs, err = parsePageToken(startPageToken) | |
| } | |
| baseQuery := ` | |
| SELECT | |
| m.module_path, | |
| m.version, | |
| m.commit_time, | |
| m.redistributable, | |
| m.has_go_mod, | |
| m.source_info, | |
| m.incompatible, | |
| m.sort_version | |
| 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) | |
| AND ($3 = '' OR (NOT m.incompatible, m.sort_version, m.module_path) <= (NOT $2, $3, $4)) | |
| ORDER BY | |
| m.incompatible, | |
| m.sort_version DESC, | |
| m.module_path DESC | |
| %s` | |
| if len(versionTypes) == 0 { | |
| return nil, "", errors.New("error: must specify at least one version type") | |
| } | |
| queryEnd := ";" | |
| if limit > 0 { | |
| queryEnd = fmt.Sprintf("LIMIT %d;", limit) | |
| } | |
| query := fmt.Sprintf(baseQuery, versionTypeExpr(versionTypes), queryEnd) | |
| var ( | |
| versions []*internal.ModuleInfo | |
| lastIncompatible bool | |
| lastSortVersion string | |
| ) | |
| collect := func(rows *sql.Rows) error { | |
| mi, err := scanModuleInfo(rows.Scan, &lastIncompatible, &lastSortVersion) | |
| if err != nil { | |
| return fmt.Errorf("row.Scan(): %v", err) | |
| } | |
| versions = append(versions, mi) | |
| return nil | |
| } | |
| args := append([]any{path}, pageTokenArgs...) | |
| if err := db.db.RunQuery(ctx, query, collect, args...); err != nil { | |
| return nil, "", err | |
| } | |
| if err := populateLatestInfos(ctx, db, versions); err != nil { | |
| return nil, "", err | |
| } | |
| // 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, lastSortVersion, versions[len(versions)-1].ModulePath) | |
| } | |
| return versions, nextPageToken, nil | |
| } | |
| // 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 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)") | |
| } | |
| startIncompatible, err := strconv.ParseBool(parts[0]) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid page token: %v", err) | |
| } | |
| return []any{startIncompatible, parts[1], parts[2]}, nil | |
| } | |
| // makePageToken constructs a page token for getPathVersions. | |
| 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, | |
| // 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) { | |
| defer 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 | |
| } |