| // 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 |
| } |