blob: 4fe881b408fa65706f6a5cec3bd3adfd7016c9a6 [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"
"fmt"
"strings"
"github.com/lib/pq"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/stdlib"
)
// orderByLatest orders paths according to the go command.
// Versions are ordered by:
// (1) release (non-incompatible)
// (2) prerelease (non-incompatible)
// (3) release, incompatible
// (4) prerelease, incompatible
// (5) pseudo
// They are then sorted based on semver, then decreasing module path length (so
// that nested modules are preferred).
const orderByLatest = `
ORDER BY
CASE
WHEN m.version_type = 'release' AND NOT m.incompatible THEN 1
WHEN m.version_type = 'prerelease' AND NOT m.incompatible THEN 2
WHEN m.version_type = 'release' THEN 3
WHEN m.version_type = 'prerelease' THEN 4
ELSE 5
END,
m.sort_version DESC,
m.module_path DESC`
// GetUnitMeta returns information about the "best" entity (module, path or directory) with
// the given path. The module and version arguments provide additional constraints.
// If the module is unknown, pass internal.UnknownModulePath; if the version is unknown, pass
// internal.LatestVersion.
//
// The rules for picking the best are:
// 1. Match the module path and or version, if they are provided;
// 2. Prefer newer module versions to older, and release to pre-release;
// 3. In the unlikely event of two paths at the same version, pick the longer module path.
func (db *DB) GetUnitMeta(ctx context.Context, path, requestedModulePath, requestedVersion string) (_ *internal.UnitMeta, err error) {
defer derrors.Wrap(&err, "DB.GetUnitMeta(ctx, %q, %q, %q)", path, requestedModulePath, requestedVersion)
defer middleware.ElapsedStat(ctx, "GetUnitMeta")()
var (
constraints []string
joinStmt string
)
args := []interface{}{path}
if requestedModulePath != internal.UnknownModulePath {
constraints = append(constraints, fmt.Sprintf("AND m.module_path = $%d", len(args)+1))
args = append(args, requestedModulePath)
}
switch requestedVersion {
case internal.LatestVersion:
case internal.MasterVersion:
joinStmt = "INNER JOIN version_map vm ON (vm.module_id = m.id)"
constraints = append(constraints, "AND vm.requested_version = 'master'")
default:
constraints = append(constraints, fmt.Sprintf("AND m.version = $%d", len(args)+1))
args = append(args, requestedVersion)
}
var (
licenseTypes []string
licensePaths []string
um = internal.UnitMeta{Path: path}
)
query := fmt.Sprintf(`
SELECT
m.module_path,
m.version,
m.commit_time,
m.source_info,
p.name,
p.redistributable,
p.license_types,
p.license_paths
FROM paths p
INNER JOIN modules m ON (p.module_id = m.id)
%s
WHERE p.path = $1
%s
%s
LIMIT 1
`, joinStmt, strings.Join(constraints, " "), orderByLatest)
err = db.db.QueryRow(ctx, query, args...).Scan(
&um.ModulePath,
&um.Version,
&um.CommitTime,
jsonbScanner{&um.SourceInfo},
&um.Name,
&um.IsRedistributable,
pq.Array(&licenseTypes),
pq.Array(&licensePaths))
switch err {
case sql.ErrNoRows:
return nil, derrors.NotFound
case nil:
lics, err := zipLicenseMetadata(licenseTypes, licensePaths)
if err != nil {
return nil, err
}
um.Licenses = lics
return &um, nil
default:
return nil, err
}
}
type dbPath struct {
id int64
path string
moduleID int64
v1Path string
name string
licenseTypes []string
licensePaths []string
redistributable bool
}
func (db *DB) getPathsInModule(ctx context.Context, modulePath, resolvedVersion string) (_ []*dbPath, err error) {
defer derrors.Wrap(&err, "DB.getPathsInModule(ctx, %q, %q)", modulePath, resolvedVersion)
query := `
SELECT
p.id,
p.path,
p.module_id,
p.v1_path,
p.name,
p.license_types,
p.license_paths,
p.redistributable
FROM
paths p
INNER JOIN
modules m
ON
p.module_id = m.id
WHERE
m.module_path = $1
AND m.version = $2
ORDER BY path;`
var paths []*dbPath
collect := func(rows *sql.Rows) error {
var p dbPath
if err := rows.Scan(&p.id, &p.path, &p.moduleID, &p.v1Path, &p.name, pq.Array(&p.licenseTypes),
pq.Array(&p.licensePaths), &p.redistributable); err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
paths = append(paths, &p)
return nil
}
if err := db.db.RunQuery(ctx, query, collect, modulePath, resolvedVersion); err != nil {
return nil, err
}
return paths, nil
}
// GetStdlibPathsWithSuffix returns information about all paths in the latest version of the standard
// library whose last component is suffix. A path that exactly match suffix is not included;
// the path must end with "/" + suffix.
//
// We are only interested in actual standard library packages: not commands, which we happen to include
// in the stdlib module, and not directories (paths that do not contain a package).
func (db *DB) GetStdlibPathsWithSuffix(ctx context.Context, suffix string) (paths []string, err error) {
defer derrors.Wrap(&err, "DB.GetStdlibPaths(ctx, %q)", suffix)
q := `
SELECT path
FROM paths
WHERE module_id = (
-- latest release version of stdlib
SELECT id
FROM modules
WHERE module_path = $1
ORDER BY
version_type = 'release' DESC,
sort_version DESC
LIMIT 1)
AND name != ''
AND path NOT LIKE 'cmd/%'
AND path LIKE '%/' || $2
ORDER BY path
`
err = db.db.RunQuery(ctx, q, func(rows *sql.Rows) error {
var p string
if err := rows.Scan(&p); err != nil {
return err
}
paths = append(paths, p)
return nil
}, stdlib.ModulePath, suffix)
if err != nil {
return nil, err
}
return paths, nil
}