blob: b3229f33639b0f517ebd3413d09ab6f90e9bd74e [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"
"database/sql"
"fmt"
"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/experiment"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/symbol"
)
func insertSymbols(ctx context.Context, db *database.DB, modulePath, version string,
pathToID map[string]int, pathToDocs map[string][]*internal.Documentation) (err error) {
defer derrors.WrapStack(&err, "insertSymbols(ctx, db, %q, %q, pathToID, pathToDocs)", modulePath, version)
if !experiment.IsActive(ctx, internal.ExperimentInsertSymbolHistory) {
return nil
}
pkgsymToID, err := upsertPackageSymbolsReturningIDs(ctx, db, modulePath, pathToID, pathToDocs)
if err != nil {
return err
}
var (
uniqueKeys = map[string]string{}
symHistoryValues []interface{}
)
for path, docs := range pathToDocs {
buildToNameToSym, err := getSymbolHistory(ctx, db, path, modulePath)
if err != nil {
return err
}
for _, doc := range docs {
builds := []internal.BuildContext{{GOOS: doc.GOOS, GOARCH: doc.GOARCH}}
if doc.GOOS == internal.All {
builds = internal.BuildContexts
}
for _, build := range builds {
nameToSymbol := buildToNameToSym[internal.BuildContext{GOOS: build.GOOS, GOARCH: build.GOARCH}]
if err := updateSymbols(doc.API, func(s *internal.Symbol) (err error) {
defer derrors.WrapStack(&err, "updateSymbols(%q)", s.Name)
if !shouldUpdateSymbolHistory(s.Name, version, nameToSymbol) {
return nil
}
pkgsym := packageSymbol{synopsis: s.Synopsis, section: s.Section}
pkgsymID := pkgsymToID[pkgsym]
if pkgsymID == 0 {
return fmt.Errorf("pkgsymID cannot be 0: %q", pkgsym)
}
// Validate that the unique constraint won't be violated.
// It is easier to debug when returning an error here as
// opposed to from the BulkUpsert statement.
key := fmt.Sprintf("%d-%s-%s", pkgsymID, build.GOOS, build.GOARCH)
if val, ok := uniqueKeys[key]; ok {
return fmt.Errorf("symbol %q exists at %q -- failed to insert symbol %q (%q) q with the same (package_symbol_id, goos, goarch)", key, val, s.Name, s.Synopsis)
}
uniqueKeys[key] = fmt.Sprintf("%q (%q)", s.Name, s.Synopsis)
symHistoryValues = append(symHistoryValues, pkgsymID, build.GOOS, build.GOARCH, version)
return nil
}); err != nil {
return err
}
}
}
}
uniqueSymCols := []string{"package_symbol_id", "goos", "goarch"}
symCols := append(uniqueSymCols, "since_version")
return db.BulkUpsert(ctx, "symbol_history", symCols, symHistoryValues, uniqueSymCols)
}
// 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]*internal.Symbol) bool {
dh, ok := oldHist[symbolName]
if !ok {
return true
}
return semver.Compare(newVersion, dh.SinceVersion) < 1
}
type packageSymbol struct {
synopsis string
section internal.SymbolSection
}
func upsertPackageSymbolsReturningIDs(ctx context.Context, db *database.DB,
modulePath string, pathToID map[string]int, pathToDocs map[string][]*internal.Documentation) (_ map[packageSymbol]int, err error) {
defer derrors.WrapStack(&err, "upsertPackageSymbolsReturningIDs(ctx, db, %q, pathToID, pathToDocs)", modulePath)
nameToID, err := upsertSymbolNamesReturningIDs(ctx, db, pathToDocs)
if err != nil {
return nil, err
}
modulePathID := pathToID[modulePath]
if modulePathID == 0 {
return nil, fmt.Errorf("modulePathID cannot be 0: %q", modulePath)
}
pkgsymToID := map[packageSymbol]int{}
collect := func(rows *sql.Rows) error {
var (
id int
section internal.SymbolSection
synopsis string
)
if err := rows.Scan(&id, &section, &synopsis); err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
pkgsymToID[packageSymbol{synopsis: synopsis, section: section}] = id
return nil
}
if err := db.RunQuery(ctx, `
SELECT
ps.id,
ps.section,
ps.synopsis
FROM package_symbols ps
INNER JOIN symbol_names sn ON ps.symbol_name_id = sn.id
WHERE module_path_id = $1;`, collect, modulePathID); err != nil {
return nil, err
}
var packageSymbols []interface{}
for path, docs := range pathToDocs {
pathID := pathToID[path]
if pathID == 0 {
return nil, fmt.Errorf("pathID cannot be 0: %q", path)
}
for _, doc := range docs {
if err := updateSymbols(doc.API, func(s *internal.Symbol) error {
ps := packageSymbol{synopsis: s.Synopsis, section: s.Section}
symID := nameToID[s.Name]
if symID == 0 {
return fmt.Errorf("pathID cannot be 0: %q", s.Name)
}
if s.ParentName == "" {
s.ParentName = s.Name
}
parentID := nameToID[s.ParentName]
if parentID == 0 {
return fmt.Errorf("pathID cannot be 0: %q", s.ParentName)
}
if _, ok := pkgsymToID[ps]; !ok {
packageSymbols = append(packageSymbols, pathID,
modulePathID, symID, parentID, s.Section, s.Kind,
s.Synopsis)
}
return nil
}); err != nil {
return nil, err
}
}
}
// The order of pkgsymcols must match that of the SELECT query in the
//collect function.
pkgsymcols := []string{"id", "section", "synopsis"}
if err := db.BulkInsertReturning(ctx, "package_symbols",
[]string{
"package_path_id",
"module_path_id",
"symbol_name_id",
"parent_symbol_name_id",
"section",
"type",
"synopsis",
}, packageSymbols, database.OnConflictDoNothing, pkgsymcols, collect); err != nil {
return nil, err
}
return pkgsymToID, nil
}
func upsertSymbolNamesReturningIDs(ctx context.Context, db *database.DB, pathToDocs map[string][]*internal.Documentation) (_ map[string]int, err error) {
defer derrors.WrapStack(&err, "upsertSymbolNamesReturningIDs")
var names []string
for _, docs := range pathToDocs {
for _, doc := range docs {
if err := updateSymbols(doc.API, func(s *internal.Symbol) error {
names = append(names, s.Name)
return nil
}); err != nil {
return nil, err
}
}
}
query := `
SELECT id, name
FROM symbol_names
WHERE name = ANY($1);`
nameToID := map[string]int{}
collect := func(rows *sql.Rows) error {
var (
id int
name string
)
if err := rows.Scan(&id, &name); err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
nameToID[name] = id
if id == 0 {
return fmt.Errorf("id can't be 0: %q", name)
}
return nil
}
if err := db.RunQuery(ctx, query, collect, pq.Array(names)); err != nil {
return nil, err
}
var values []interface{}
for _, name := range names {
if _, ok := nameToID[name]; !ok {
values = append(values, name)
}
}
if err := db.BulkInsertReturning(ctx, "symbol_names", []string{"name"},
values, database.OnConflictDoNothing, []string{"id", "name"}, collect); err != nil {
return nil, err
}
return nameToID, nil
}
func getSymbolHistory(ctx context.Context, db *database.DB, packagePath, modulePath string) (_ map[internal.BuildContext]map[string]*internal.Symbol, err error) {
defer derrors.Wrap(&err, "getSymbolHistoryForPath(ctx, db, %q, %q)", packagePath, modulePath)
query := `
SELECT
s1.name AS symbol_name,
s2.name AS parent_symbol_name,
ps.section,
ps.type,
ps.synopsis,
sh.since_version,
sh.goos,
sh.goarch
FROM symbol_history sh
INNER JOIN package_symbols ps ON sh.package_symbol_id = ps.id
INNER JOIN symbol_names s1 ON ps.symbol_name_id = s1.id
INNER JOIN symbol_names s2 ON ps.parent_symbol_name_id = s2.id
INNER JOIN paths p1 ON ps.package_path_id = p1.id
INNER JOIN paths p2 ON ps.module_path_id = p2.id
WHERE p1.path = $1 AND p2.path = $2;`
// Map from GOOS/GOARCH to (map from symbol name to symbol).
buildToNameToSym := map[internal.BuildContext]map[string]*internal.Symbol{}
collect := func(rows *sql.Rows) error {
var (
sh internal.Symbol
)
if err := rows.Scan(
&sh.Name,
&sh.ParentName,
&sh.Section,
&sh.Kind,
&sh.Synopsis,
&sh.SinceVersion,
&sh.GOOS,
&sh.GOARCH,
); err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
nameToSym, ok := buildToNameToSym[internal.BuildContext{GOOS: sh.GOOS, GOARCH: sh.GOARCH}]
if !ok {
nameToSym = map[string]*internal.Symbol{}
buildToNameToSym[internal.BuildContext{GOOS: sh.GOOS, GOARCH: sh.GOARCH}] = nameToSym
}
nameToSym[sh.Name] = &sh
return nil
}
if err := db.RunQuery(ctx, query, collect, packagePath, modulePath); err != nil {
return nil, err
}
return buildToNameToSym, nil
}
func updateSymbols(symbols []*internal.Symbol, updateFunc func(s *internal.Symbol) error) error {
for _, s := range symbols {
if err := updateFunc(s); err != nil {
return err
}
for _, s := range s.Children {
if err := updateFunc(s); err != nil {
return err
}
}
}
return nil
}
// CompareStdLib is a helper function for comparing the output of
// getSymbolHistory and symbol.ParsePackageAPIInfo. This is only meant for use
// locally for testing purposes.
func (db *DB) CompareStdLib(ctx context.Context) (map[string][]string, error) {
apiVersions, err := symbol.ParsePackageAPIInfo()
if err != nil {
return nil, err
}
pkgToErrors := map[string][]string{}
for path := range apiVersions {
hist, err := getSymbolHistory(ctx, db.db, path, stdlib.ModulePath)
if err != nil {
return nil, err
}
// symbol.ParsePackageAPIInfo does not support OS/ARCH-dependent symbols.
data := hist[internal.BuildContext{GOOS: "linux", GOARCH: "amd64"}]
errs := symbol.CompareStdLib(path, apiVersions[path], data)
if len(errs) > 0 {
pkgToErrors[path] = errs
}
}
return pkgToErrors, nil
}