blob: d33fda19ffaade225be4f97cdc7fd31afdf0bf2c [file] [log] [blame]
// Copyright 2021 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"
"fmt"
"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/experiment"
"golang.org/x/pkgsite/internal/version"
)
// upsertSymbolHistory upserts data into the symbol_history table.
func upsertSymbolHistory(ctx context.Context, ddb *database.DB,
modulePath, ver string,
nameToID map[string]int,
pathToID map[string]int,
pathToPkgsymID map[string]map[packageSymbol]int,
pathToDocIDToDoc map[string]map[int]*internal.Documentation,
) (err error) {
defer derrors.WrapStack(&err, "upsertSymbolHistory")
versionType, err := version.ParseType(ver)
if err != nil {
return err
}
if versionType != version.TypeRelease || version.IsIncompatible(ver) {
return nil
}
for packagePath, docIDToDoc := range pathToDocIDToDoc {
sh, err := GetSymbolHistoryFromTable(ctx, ddb, packagePath, modulePath)
if err != nil {
return err
}
for _, doc := range docIDToDoc {
var values []interface{}
builds := []internal.BuildContext{{GOOS: doc.GOOS, GOARCH: doc.GOARCH}}
if doc.GOOS == internal.All {
builds = internal.BuildContexts
}
for _, b := range builds {
dbNameToVersion := map[string]string{}
for _, v := range sh.Versions() {
nts := sh.SymbolsAtVersion(v)
for name, stu := range nts {
for _, us := range stu {
if us.SupportsBuild(b) {
dbNameToVersion[name] = v
}
}
}
}
seen := map[string]bool{}
if err := updateSymbols(doc.API, func(sm *internal.SymbolMeta) error {
// While a package with duplicate symbol names won't build,
// the documentation for these packages are currently
// rendered on pkg.go.dev, so doc.API may contain more than
// one symbol with the same name.
//
// For the purpose of symbol_history, just use the first
// symbol name we see.
if seen[sm.Name] {
return nil
}
seen[sm.Name] = true
if shouldUpdateSymbolHistory(sm.Name, ver, dbNameToVersion) {
values, err = appendSymbolHistoryRow(sm, values,
packagePath, modulePath, ver, b, pathToID, nameToID,
pathToPkgsymID)
if err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
}
cols := []string{
"symbol_name_id",
"parent_symbol_name_id",
"package_path_id",
"module_path_id",
"package_symbol_id",
"since_version",
"sort_version",
"goos",
"goarch",
}
for _, table := range []string{
"symbol_history",
"new_symbol_history",
} {
if experiment.IsActive(ctx, internal.ExperimentSkipInsertSymbols) {
if table == "symbol_history" {
// Skip this table because we can't insert into
// package_symbol_id due to integer overflow issues.
continue
}
}
// Only insert if the table exists.
// TODO(jba): remove after new_symbol_history is renamed to symbol_history.
if exists, err := tableExists(ctx, ddb, table); err != nil {
return err
} else if !exists {
continue
}
if err := ddb.BulkInsert(ctx, table, cols, values,
fmt.Sprintf(`ON CONFLICT (package_path_id, module_path_id, symbol_name_id, goos, goarch)
DO UPDATE
SET
symbol_name_id=excluded.symbol_name_id,
parent_symbol_name_id=excluded.parent_symbol_name_id,
package_path_id=excluded.package_path_id,
module_path_id=excluded.module_path_id,
package_symbol_id=excluded.package_symbol_id,
since_version=excluded.since_version,
sort_version=excluded.sort_version,
goos=excluded.goos,
goarch=excluded.goarch
WHERE
%s.sort_version > excluded.sort_version`, table)); err != nil {
return err
}
}
}
}
return nil
}
// tableExists reports whether the table with the given name exists in the database.
func tableExists(ctx context.Context, ddb *database.DB, table string) (bool, error) {
var x *string
if err := ddb.QueryRow(ctx, `SELECT to_regclass($1)`, table).Scan(&x); err != nil {
return false, err
}
return x != nil, nil
}
func appendSymbolHistoryRow(sm *internal.SymbolMeta, values []interface{},
packagePath, modulePath, ver string, build internal.BuildContext,
pathToID, symToID map[string]int,
pathToPkgsymID map[string]map[packageSymbol]int) (_ []interface{}, err error) {
defer derrors.WrapStack(&err, "appendSymbolHistoryRow(%q, %q, %q, %q)", sm.Name, packagePath, modulePath, ver)
symbolID := symToID[sm.Name]
if symbolID == 0 {
return nil, fmt.Errorf("symbolID cannot be 0: %q", sm.Name)
}
if sm.ParentName == "" {
sm.ParentName = sm.Name
}
parentID := symToID[sm.ParentName]
if parentID == 0 {
return nil, fmt.Errorf("parentSymbolID cannot be 0: %q", sm.ParentName)
}
packagePathID := pathToID[packagePath]
if packagePathID == 0 {
return nil, fmt.Errorf("packagePathID cannot be 0: %q", packagePathID)
}
modulePathID := pathToID[modulePath]
if modulePathID == 0 {
return nil, fmt.Errorf("modulePathID cannot be 0: %q", modulePathID)
}
pkgsymID := pathToPkgsymID[packagePath][packageSymbol{synopsis: sm.Synopsis, name: sm.Name, parentName: sm.ParentName}]
return append(values,
symbolID,
parentID,
packagePathID,
modulePathID,
pkgsymID,
ver,
version.ForSorting(ver),
build.GOOS,
build.GOARCH), nil
}
// shouldUpdateSymbolHistory reports whether the row for the given symbolName
// should be updated. oldHist contains all of the current symbols in the
// database for the same package and GOOS/GOARCH.
//
// shouldUpdateSymbolHistory reports true if the symbolName does not currently
// exist, or if the newVersion is older than or equal to the current database version.
func shouldUpdateSymbolHistory(symbolName, newVersion string, oldHist map[string]string) bool {
oldVersion, ok := oldHist[symbolName]
if !ok {
return true
}
return semver.Compare(newVersion, oldVersion) <= 0
}