| // Copyright 2019 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" | |
| "sort" | |
| "strings" | |
| "github.com/Masterminds/squirrel" | |
| "github.com/lib/pq" | |
| "golang.org/x/pkgsite/internal" | |
| "golang.org/x/pkgsite/internal/database" | |
| "golang.org/x/pkgsite/internal/derrors" | |
| "golang.org/x/pkgsite/internal/licenses" | |
| "golang.org/x/pkgsite/internal/middleware/stats" | |
| "golang.org/x/pkgsite/internal/stdlib" | |
| "golang.org/x/pkgsite/internal/version" | |
| ) | |
| // 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 as follows. | |
| // | |
| // 1. If the version is known but the module path is not, choose the longest module path | |
| // at that version that contains fullPath. | |
| // | |
| // 2. Otherwise, find the latest "good" version (in the modules table) that contains fullPath. | |
| // | |
| // a. First, follow the algorithm of the go command: prefer longer module paths, and | |
| // find the latest unretracted version, using semver but preferring release to pre-release. | |
| // b. If no modules have latest-version information, find the latest by sorting the versions | |
| // we do have: again first by module path length, then by version. | |
| func (db *DB) GetUnitMeta(ctx context.Context, fullPath, requestedModulePath, requestedVersion string) (_ *internal.UnitMeta, err error) { | |
| defer derrors.WrapStack(&err, "DB.GetUnitMeta(ctx, %q, %q, %q)", fullPath, requestedModulePath, requestedVersion) | |
| defer stats.Elapsed(ctx, "DB.GetUnitMeta")() | |
| modulePath := requestedModulePath | |
| v := requestedVersion | |
| var lmv *internal.LatestModuleVersions | |
| if requestedVersion == version.Latest { | |
| modulePath, v, lmv, err = db.getLatestUnitVersion(ctx, fullPath, requestedModulePath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| return db.getUnitMetaWithKnownVersion(ctx, fullPath, modulePath, v, lmv) | |
| } | |
| func (db *DB) getUnitMetaWithKnownVersion(ctx context.Context, fullPath, modulePath, version string, lmv *internal.LatestModuleVersions) (_ *internal.UnitMeta, err error) { | |
| defer derrors.WrapStack(&err, "getUnitMetaWithKnownVersion") | |
| defer stats.Elapsed(ctx, "getUnitMetaWithKnownVersion")() | |
| query := squirrel.Select( | |
| "m.module_path", | |
| "m.version", | |
| "m.commit_time", | |
| "m.source_info", | |
| "m.has_go_mod", | |
| "m.redistributable", | |
| "u.name"). | |
| From("modules m"). | |
| Join("units u on u.module_id = m.id"). | |
| Join("paths p ON p.id = u.path_id").Where(squirrel.Eq{"p.path": fullPath}). | |
| PlaceholderFormat(squirrel.Dollar) | |
| if internal.DefaultBranches[version] || stdlib.SupportedBranches[version] { | |
| query = query. | |
| Join("version_map vm ON m.id = vm.module_id"). | |
| Where("vm.requested_version = ?", version) | |
| } else { | |
| query = query.Where(squirrel.Eq{"version": version}) | |
| } | |
| if modulePath == internal.UnknownModulePath { | |
| candidates := internal.CandidateModulePaths(fullPath) | |
| // If we don't know the module, look for the one with the longest series path. | |
| query = query.Where(squirrel.Eq{"m.module_path": candidates}). | |
| OrderBy("m.series_path DESC"). | |
| Limit(1) | |
| } else { | |
| query = query.Where(squirrel.Eq{"m.module_path": modulePath}) | |
| } | |
| q, args, err := query.ToSql() | |
| if err != nil { | |
| return nil, err | |
| } | |
| var ( | |
| um = internal.UnitMeta{Path: fullPath} | |
| ) | |
| err = db.db.QueryRow(ctx, q, args...).Scan( | |
| &um.ModulePath, | |
| &um.Version, | |
| &um.CommitTime, | |
| jsonbScanner{&um.SourceInfo}, | |
| &um.HasGoMod, | |
| &um.ModuleInfo.IsRedistributable, | |
| &um.Name) | |
| if err == sql.ErrNoRows { | |
| return nil, derrors.NotFound | |
| } | |
| if err != nil { | |
| return nil, err | |
| } | |
| // If we don't have the latest version information, try to get it. | |
| // We can be here if there is really no info (in which case we are repeating | |
| // some work, but it's fast), or if we are ignoring the info (for instance, | |
| // if all versions were retracted). | |
| if lmv == nil { | |
| lmv, err = db.GetLatestModuleVersions(ctx, um.ModulePath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| if lmv != nil { | |
| lmv.PopulateModuleInfo(&um.ModuleInfo) | |
| } | |
| return &um, nil | |
| } | |
| // getLatestUnitVersion gets the latest version of requestedModulePath that contains fullPath. | |
| // See GetUnitMeta for more details. | |
| func (db *DB) getLatestUnitVersion(ctx context.Context, fullPath, requestedModulePath string) ( | |
| modulePath, latestVersion string, lmv *internal.LatestModuleVersions, err error) { | |
| defer derrors.WrapStack(&err, "getLatestUnitVersion(%q, %q)", fullPath, requestedModulePath) | |
| defer stats.Elapsed(ctx, "getLatestUnitVersion")() | |
| modPaths := []string{requestedModulePath} | |
| // If we don't know the module path, try each possible module path from longest to shortest. | |
| if requestedModulePath == internal.UnknownModulePath { | |
| modPaths = internal.CandidateModulePaths(fullPath) | |
| } | |
| // Get latest-version information for all possible modules, from longest | |
| // to shortest path. | |
| lmvs, err := db.getMultiLatestModuleVersions(ctx, modPaths) | |
| if err != nil { | |
| return "", "", nil, err | |
| } | |
| for _, lmv = range lmvs { | |
| // Collect all the versions of this module that contain fullPath. | |
| query := squirrel.Select("m.version"). | |
| From("modules m"). | |
| Join("units u on u.module_id = m.id"). | |
| Join("paths p ON p.id = u.path_id"). | |
| Where(squirrel.Eq{"m.module_path": lmv.ModulePath}). | |
| Where(squirrel.Eq{"p.path": fullPath}) | |
| q, args, err := query.PlaceholderFormat(squirrel.Dollar).ToSql() | |
| if err != nil { | |
| return "", "", nil, err | |
| } | |
| allVersions, err := database.Collect1[string](ctx, db.db, q, args...) | |
| if err != nil { | |
| return "", "", nil, err | |
| } | |
| // Remove retracted versions. | |
| unretractedVersions := version.RemoveIf(allVersions, lmv.IsRetracted) | |
| // If there are no unretracted versions, move on. If we fall out of the | |
| // loop we will pick the latest retracted version. | |
| if len(unretractedVersions) == 0 { | |
| continue | |
| } | |
| // Choose the latest version. | |
| // If the cooked latest version is compatible, then by the logic of | |
| // internal/version.Latest (which matches the go command), either | |
| // incompatible versions should be ignored or there were no incompatible | |
| // versions. In either case, remove them. | |
| if !version.IsIncompatible(lmv.CookedVersion) { | |
| unretractedVersions = version.RemoveIf(unretractedVersions, version.IsIncompatible) | |
| } | |
| // The good version should never be later than the cooked version. | |
| // https://golang.org/issue/43265 documents how that could happen: | |
| // 1. A pseudo-version is created off of a tagged version. In this case, | |
| // golang.zx2c4.com/wireguard@v0.0.20201119-0.20210128142622-6a128dde71d9. | |
| // 2. All tagged versions are retracted. | |
| // 3. A new pseudo-version is published. In this case, v0.0.0-20210506092213-60a26371f42f. | |
| // Technically, the first pseudo-version is higher than the second, so it | |
| // is the latest good version. But the proxy's latest endpoint returns | |
| // the second pseudo-version, so that is the cooked version (and what the | |
| // go command will download). | |
| if lmv.CookedVersion != "" { | |
| unretractedVersions = version.RemoveIf(unretractedVersions, func(v string) bool { | |
| return version.Later(v, lmv.CookedVersion) | |
| }) | |
| } | |
| latestVersion = version.LatestOf(unretractedVersions) | |
| break | |
| } | |
| if latestVersion != "" { | |
| return lmv.ModulePath, latestVersion, lmv, nil | |
| } | |
| // If we don't have latest-version info for any path (or there are no | |
| // unretracted versions for paths where we do), fall back to finding the | |
| // latest good version from the longest path. We can't determine | |
| // deprecations or retractions, and the "go get" command won't download the | |
| // module unless a specific version is supplied. But we can still show the | |
| // latest version we have. | |
| query := squirrel.Select("m.module_path", "m.version"). | |
| From("modules m"). | |
| Join("units u on u.module_id = m.id"). | |
| Join("paths p ON p.id = u.path_id"). | |
| Where(squirrel.Eq{"p.path": fullPath}). | |
| // Like the go command, order first by path length, then by release | |
| // version, then prerelease. Without latest-version information, we | |
| // ignore all adjustments for incompatible and retracted versions. | |
| OrderBy("m.series_path DESC", "m.version_type = 'release' DESC", "m.sort_version DESC"). | |
| Limit(1) | |
| q, args, err := query.PlaceholderFormat(squirrel.Dollar).ToSql() | |
| if err != nil { | |
| return "", "", nil, err | |
| } | |
| err = db.db.QueryRow(ctx, q, args...).Scan(&modulePath, &latestVersion) | |
| if err == sql.ErrNoRows { | |
| return "", "", nil, derrors.NotFound | |
| } | |
| if err != nil { | |
| return "", "", nil, err | |
| } | |
| return modulePath, latestVersion, nil, nil | |
| } | |
| // GetUnit returns a unit from the database, along with all of the data | |
| // associated with that unit. | |
| // If bc is not nil, get only the Documentation that matches it (or nil if none do). | |
| func (db *DB) GetUnit(ctx context.Context, um *internal.UnitMeta, fields internal.FieldSet, bc internal.BuildContext) (_ *internal.Unit, err error) { | |
| defer derrors.WrapStack(&err, "GetUnit(ctx, %q, %q, %q, %v)", um.Path, um.ModulePath, um.Version, bc) | |
| u := &internal.Unit{UnitMeta: *um} | |
| if fields&internal.WithMain != 0 { | |
| u, err = db.getUnitWithAllFields(ctx, um, fields, bc) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| if fields&internal.WithImports == 0 && | |
| fields&internal.WithLicenses == 0 { | |
| return u, nil | |
| } | |
| defer stats.Elapsed(ctx, "GetUnit")() | |
| unitID, isRedistributable, err := db.getUnitID(ctx, um.Path, um.ModulePath, um.Version) | |
| if err != nil { | |
| return nil, err | |
| } | |
| u.IsRedistributable = isRedistributable | |
| if fields&internal.WithImports != 0 { | |
| imports, err := db.getImports(ctx, unitID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(imports) > 0 { | |
| u.Imports = imports | |
| u.NumImports = len(imports) | |
| } | |
| } | |
| if fields&internal.WithLicenses != 0 { | |
| lics, err := db.getLicenses(ctx, u.Path, u.ModulePath, unitID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| u.LicenseContents = lics | |
| } | |
| if db.bypassLicenseCheck { | |
| u.IsRedistributable = true | |
| } else { | |
| u.RemoveNonRedistributableData() | |
| } | |
| return u, nil | |
| } | |
| // GetModulePackages returns a list of packages in the given module version. | |
| func (db *DB) GetModulePackages(ctx context.Context, modulePath, requestedVersion string) ([]*internal.PackageMeta, error) { | |
| resolvedVersion := requestedVersion | |
| if requestedVersion == version.Latest { | |
| lmv, err := db.GetLatestModuleVersions(ctx, modulePath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if lmv == nil { | |
| return nil, fmt.Errorf("%w: module %s not found", derrors.NotFound, modulePath) | |
| } | |
| resolvedVersion = lmv.GoodVersion | |
| } | |
| return getPackagesInUnit(ctx, db.db, modulePath, modulePath, resolvedVersion, -1, db.bypassLicenseCheck) | |
| } | |
| func (db *DB) getUnitID(ctx context.Context, fullPath, modulePath, resolvedVersion string) (_ int, _ bool, err error) { | |
| defer derrors.WrapStack(&err, "getUnitID(ctx, %q, %q, %q)", fullPath, modulePath, resolvedVersion) | |
| defer stats.Elapsed(ctx, "getUnitID")() | |
| var unitID int | |
| var isRedistributable bool | |
| query := ` | |
| SELECT u.id, u.redistributable | |
| FROM units u | |
| INNER JOIN paths p ON (p.id = u.path_id) | |
| INNER JOIN modules m ON (u.module_id = m.id) | |
| WHERE | |
| p.path = $1 | |
| AND m.module_path = $2 | |
| AND m.version = $3;` | |
| err = db.db.QueryRow(ctx, query, fullPath, modulePath, resolvedVersion).Scan(&unitID, &isRedistributable) | |
| switch err { | |
| case sql.ErrNoRows: | |
| return 0, false, derrors.NotFound | |
| case nil: | |
| return unitID, isRedistributable, nil | |
| default: | |
| return 0, false, err | |
| } | |
| } | |
| // getImports returns the imports corresponding to unitID. | |
| func (db *DB) getImports(ctx context.Context, unitID int) (_ []string, err error) { | |
| defer derrors.WrapStack(&err, "getImports(ctx, %d)", unitID) | |
| defer stats.Elapsed(ctx, "getImports")() | |
| query := ` | |
| SELECT p.path | |
| FROM paths p INNER JOIN imports i ON p.id = i.to_path_id | |
| WHERE i.unit_id = $1` | |
| return database.Collect1[string](ctx, db.db, query, unitID) | |
| } | |
| // getPackagesInUnit returns all of the packages in a unit from a | |
| // module_id, including the package that lives at fullPath, if present. | |
| func (db *DB) getPackagesInUnit(ctx context.Context, fullPath string, moduleID int) (_ []*internal.PackageMeta, err error) { | |
| return getPackagesInUnit(ctx, db.db, fullPath, "", "", moduleID, db.bypassLicenseCheck) | |
| } | |
| func getPackagesInUnit(ctx context.Context, db *database.DB, fullPath, modulePath, resolvedVersion string, moduleID int, bypassLicenseCheck bool) (_ []*internal.PackageMeta, err error) { | |
| defer derrors.WrapStack(&err, "getPackagesInUnit(ctx, %q, %q, %q, %d)", fullPath, modulePath, resolvedVersion, moduleID) | |
| defer stats.Elapsed(ctx, "getPackagesInUnit")() | |
| queryBuilder := squirrel.Select( | |
| "p.path", | |
| "u.name", | |
| "u.redistributable", | |
| "d.synopsis", | |
| "d.GOOS", | |
| "d.GOARCH", | |
| "u.license_types", | |
| "u.license_paths", | |
| ).From("units u") | |
| if moduleID != -1 { | |
| queryBuilder = queryBuilder.Join("paths p ON p.id = u.path_id"). | |
| LeftJoin("documentation d ON d.unit_id = u.id"). | |
| Where(squirrel.Eq{"u.module_id": moduleID}) | |
| } else { | |
| queryBuilder = queryBuilder. | |
| Join("modules m ON u.module_id = m.id"). | |
| Join("paths p ON p.id = u.path_id"). | |
| LeftJoin("documentation d ON d.unit_id = u.id"). | |
| Where(squirrel.Eq{"m.module_path": modulePath}). | |
| Where(squirrel.Eq{"m.version": resolvedVersion}) | |
| } | |
| queryBuilder = queryBuilder.Where(squirrel.NotEq{"u.name": ""}) | |
| query, args, err := queryBuilder.PlaceholderFormat(squirrel.Dollar).ToSql() | |
| if err != nil { | |
| return nil, err | |
| } | |
| // If a package has more than build context (GOOS/GOARCH pair), it will have | |
| // more than one row in documentation, and this query will produce multiple | |
| // rows for that package. If we could sort the build contexts in SQL we | |
| // could deal with that in the query, but we must sort in code, so we read | |
| // all the rows and pick the right one afterwards. | |
| type pmbc struct { | |
| pm *internal.PackageMeta | |
| bc internal.BuildContext | |
| } | |
| packagesByPath := map[string][]pmbc{} | |
| collect := func(rows *sql.Rows) error { | |
| var ( | |
| pkg internal.PackageMeta | |
| licenseTypes []string | |
| licensePaths []string | |
| bc internal.BuildContext | |
| ) | |
| if err := rows.Scan( | |
| &pkg.Path, | |
| &pkg.Name, | |
| &pkg.IsRedistributable, | |
| database.NullIsEmpty(&pkg.Synopsis), | |
| database.NullIsEmpty(&bc.GOOS), | |
| database.NullIsEmpty(&bc.GOARCH), | |
| pq.Array(&licenseTypes), | |
| pq.Array(&licensePaths), | |
| ); err != nil { | |
| return fmt.Errorf("row.Scan(): %v", err) | |
| } | |
| if fullPath == stdlib.ModulePath || pkg.Path == fullPath || strings.HasPrefix(pkg.Path, fullPath+"/") { | |
| lics, err := zipLicenseMetadata(licenseTypes, licensePaths) | |
| if err != nil { | |
| return err | |
| } | |
| pkg.Licenses = lics | |
| packagesByPath[pkg.Path] = append(packagesByPath[pkg.Path], pmbc{&pkg, bc}) | |
| } | |
| return nil | |
| } | |
| if err := db.RunQuery(ctx, query, collect, args...); err != nil { | |
| return nil, err | |
| } | |
| if len(packagesByPath) == 0 { | |
| return nil, derrors.NotFound | |
| } | |
| var packages []*internal.PackageMeta | |
| for _, ps := range packagesByPath { | |
| sort.Slice(ps, func(i, j int) bool { return internal.CompareBuildContexts(ps[i].bc, ps[j].bc) < 0 }) | |
| packages = append(packages, ps[0].pm) | |
| } | |
| sort.Slice(packages, func(i, j int) bool { return packages[i].Path < packages[j].Path }) | |
| for _, p := range packages { | |
| if bypassLicenseCheck { | |
| p.IsRedistributable = true | |
| } else { | |
| p.RemoveNonRedistributableData() | |
| } | |
| } | |
| return packages, nil | |
| } | |
| // unitContext contains information for a unit that is used to fetch its | |
| // documentation or symbols. | |
| type unitContext struct { | |
| unitID int | |
| pathID int | |
| moduleID int | |
| isRedistributable bool | |
| bcs []internal.BuildContext | |
| bestBC internal.BuildContext | |
| docID int | |
| licenseMetas []*licenses.Metadata | |
| } | |
| // getUnitContext returns a unitContext for the given package path, module path, | |
| // version and build context. It finds all available build contexts for the | |
| // unit, selects the best matching one, and identifies the corresponding | |
| // documentation ID. | |
| func (db *DB) getUnitContext(ctx context.Context, pkgPath, modulePath, version string, bc internal.BuildContext) (_ *unitContext, err error) { | |
| defer derrors.WrapStack(&err, "getUnitContext(ctx, %q, %q, %q, %v)", pkgPath, modulePath, version, bc) | |
| var ( | |
| uc = &unitContext{} | |
| // Map from build context to documentation ID. | |
| docIDs = make(map[internal.BuildContext]int) | |
| ) | |
| err = db.db.RunQuery(ctx, ` | |
| SELECT d.goos, d.goarch, u.id, p.id, u.module_id, u.license_types, u.license_paths, u.redistributable, d.id | |
| FROM units u | |
| INNER JOIN paths p ON p.id = u.path_id | |
| INNER JOIN modules m ON m.id = u.module_id | |
| LEFT JOIN documentation d ON d.unit_id = u.id | |
| WHERE | |
| p.path = $1 | |
| AND m.module_path = $2 | |
| AND m.version = $3 | |
| `, func(rows *sql.Rows) error { | |
| var ( | |
| rowBC internal.BuildContext | |
| docID sql.NullInt64 | |
| ) | |
| // We need to scan all columns, but some are unit-level (constant across rows) | |
| // and some are documentation-level (vary across rows). | |
| var ( | |
| uIDCol, pIDCol, mIDCol int | |
| licTypesCol, licPathsCol []string | |
| isRedistCol bool | |
| ) | |
| if err := rows.Scan( | |
| database.NullIsEmpty(&rowBC.GOOS), | |
| database.NullIsEmpty(&rowBC.GOARCH), | |
| &uIDCol, | |
| &pIDCol, | |
| &mIDCol, | |
| pq.Array(&licTypesCol), | |
| pq.Array(&licPathsCol), | |
| &isRedistCol, | |
| &docID, | |
| ); err != nil { | |
| return err | |
| } | |
| // Assign unit-level metadata only once. | |
| if uc.unitID == 0 { | |
| uc.unitID = uIDCol | |
| uc.pathID = pIDCol | |
| uc.moduleID = mIDCol | |
| uc.isRedistributable = isRedistCol | |
| if db.bypassLicenseCheck { | |
| uc.isRedistributable = true | |
| } | |
| lics, err := zipLicenseMetadata(licTypesCol, licPathsCol) | |
| if err != nil { | |
| return err | |
| } | |
| uc.licenseMetas = lics | |
| } | |
| if rowBC.GOOS != "" && rowBC.GOARCH != "" { | |
| uc.bcs = append(uc.bcs, rowBC) | |
| if docID.Valid { | |
| docIDs[rowBC] = int(docID.Int64) | |
| } | |
| } | |
| return nil | |
| }, pkgPath, modulePath, version) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if uc.unitID == 0 { | |
| return nil, derrors.NotFound | |
| } | |
| uc.bcs = internal.SortedBuildContexts(uc.bcs) | |
| var ok bool | |
| uc.bestBC, ok = internal.MatchingBuildContext(uc.bcs, bc) | |
| if ok { | |
| uc.docID = docIDs[uc.bestBC] | |
| } | |
| return uc, nil | |
| } | |
| func (db *DB) getUnitWithAllFields(ctx context.Context, um *internal.UnitMeta, fields internal.FieldSet, bc internal.BuildContext) (_ *internal.Unit, err error) { | |
| defer derrors.WrapStack(&err, "getUnitWithAllFields(ctx, %q, %q, %q)", um.Path, um.ModulePath, um.Version) | |
| defer stats.Elapsed(ctx, "getUnitWithAllFields")() | |
| uc, err := db.getUnitContext(ctx, um.Path, um.ModulePath, um.Version, bc) | |
| if err != nil { | |
| return nil, err | |
| } | |
| docSelect := "CAST(NULL AS bytea)," | |
| if fields&internal.WithDocsSource != 0 { | |
| docSelect = "d.source," | |
| } | |
| query := fmt.Sprintf(` | |
| SELECT | |
| r.file_path, | |
| r.contents, | |
| d.synopsis, | |
| %s | |
| COALESCE(( | |
| SELECT COUNT(unit_id) | |
| FROM imports | |
| WHERE unit_id = u.id | |
| GROUP BY unit_id | |
| ), 0) AS num_imports, | |
| COALESCE(( | |
| SELECT imported_by_count | |
| FROM search_documents | |
| -- Only package_path_id is needed b/c it is the PK for | |
| -- search_documents. | |
| WHERE package_path_id = $1 | |
| ), 0) AS num_imported_by | |
| FROM units u | |
| LEFT JOIN readmes r | |
| ON r.unit_id = u.id | |
| LEFT JOIN documentation d | |
| ON d.id = $3 | |
| WHERE u.id = $2 | |
| `, docSelect) | |
| var ( | |
| r internal.Readme | |
| u internal.Unit | |
| ) | |
| u.BuildContexts = uc.bcs | |
| doc := &internal.Documentation{GOOS: uc.bestBC.GOOS, GOARCH: uc.bestBC.GOARCH} | |
| end := stats.Elapsed(ctx, "getUnitWithAllFields-readme-and-imports") | |
| var docID any | |
| if uc.docID != 0 { | |
| docID = uc.docID | |
| } | |
| err = db.db.QueryRow(ctx, query, uc.pathID, uc.unitID, docID).Scan( | |
| database.NullIsEmpty(&r.Filepath), | |
| database.NullIsEmpty(&r.Contents), | |
| database.NullIsEmpty(&doc.Synopsis), | |
| &doc.Source, | |
| &u.NumImports, | |
| &u.NumImportedBy, | |
| ) | |
| switch err { | |
| case sql.ErrNoRows: | |
| // Neither a README nor documentation; that's OK, continue. | |
| case nil: | |
| if r.Filepath != "" && um.ModulePath != stdlib.ModulePath { | |
| u.Readme = &r | |
| } | |
| if doc.GOOS != "" { | |
| u.Documentation = []*internal.Documentation{doc} | |
| } | |
| default: | |
| return nil, err | |
| } | |
| end() | |
| // Get other info. | |
| pkgs, err := db.getPackagesInUnit(ctx, um.Path, uc.moduleID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| u.Subdirectories = pkgs | |
| u.UnitMeta = *um | |
| u.Licenses = uc.licenseMetas | |
| u.IsRedistributable = uc.isRedistributable | |
| if um.IsPackage() && !um.IsCommand() && uc.bestBC.GOOS != "" { | |
| u.SymbolHistory, err = GetSymbolHistoryForBuildContext(ctx, db.db, uc.pathID, um.ModulePath, uc.bestBC) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| return &u, nil | |
| } | |
| type dbPath struct { | |
| id int64 | |
| path string | |
| moduleID int64 | |
| name string | |
| licenseTypes []string | |
| licensePaths []string | |
| redistributable bool | |
| } | |
| func (db *DB) getPathsInModule(ctx context.Context, modulePath, resolvedVersion string) (_ []*dbPath, err error) { | |
| defer derrors.WrapStack(&err, "DB.getPathsInModule(ctx, %q, %q)", modulePath, resolvedVersion) | |
| query := ` | |
| SELECT | |
| u.id, | |
| p.path, | |
| u.module_id, | |
| u.name, | |
| u.license_types, | |
| u.license_paths, | |
| u.redistributable | |
| FROM | |
| units u | |
| INNER JOIN | |
| paths p | |
| ON | |
| p.id = u.path_id | |
| INNER JOIN | |
| modules m | |
| ON | |
| u.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.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 | |
| } | |
| // GetModuleReadme returns the README corresponding to the modulePath and version. | |
| func (db *DB) GetModuleReadme(ctx context.Context, modulePath, resolvedVersion string) (_ *internal.Readme, err error) { | |
| return getModuleReadme(ctx, db.db, modulePath, resolvedVersion) | |
| } | |
| func getModuleReadme(ctx context.Context, db *database.DB, modulePath, resolvedVersion string) (_ *internal.Readme, err error) { | |
| defer derrors.WrapStack(&err, "getModuleReadme(ctx, %q, %q)", modulePath, resolvedVersion) | |
| var readme internal.Readme | |
| err = db.QueryRow(ctx, ` | |
| SELECT file_path, contents | |
| FROM modules m | |
| INNER JOIN units u | |
| ON u.module_id = m.id | |
| INNER JOIN paths p | |
| ON u.path_id = p.id | |
| INNER JOIN readmes r | |
| ON u.id = r.unit_id | |
| WHERE | |
| m.module_path=$1 | |
| AND m.version=$2 | |
| AND m.module_path=p.path`, modulePath, resolvedVersion).Scan(&readme.Filepath, &readme.Contents) | |
| switch err { | |
| case sql.ErrNoRows: | |
| return nil, derrors.NotFound | |
| case nil: | |
| return &readme, nil | |
| default: | |
| return nil, err | |
| } | |
| } |