internal/postgres: handle different SymbolMeta for different build contexts
GetSymbolHistory now returns a SymbolHistory type.
SymbolHistory is a map of version to name to SymbolMeta to UnitSymbol.
It takes into account cases where the SymbolMeta may be different for
different build contexts.
For golang/go#37102
Change-Id: I0cf757863d89fa0b95ed3a63ca1bfec6ee033c54
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/316371
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/postgres/package_symbol.go b/internal/postgres/package_symbol.go
index 0227a51..09155bc 100644
--- a/internal/postgres/package_symbol.go
+++ b/internal/postgres/package_symbol.go
@@ -15,10 +15,78 @@
"golang.org/x/pkgsite/internal/middleware"
)
+// getPackageSymbols returns all of the symbols for a given package path and module path.
+func getPackageSymbols(ctx context.Context, ddb *database.DB, packagePath, modulePath string,
+) (_ *internal.SymbolHistory, err error) {
+ defer derrors.Wrap(&err, "getPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
+ defer middleware.ElapsedStat(ctx, "getPackageSymbols")()
+ query := `
+ SELECT
+ s1.name AS symbol_name,
+ s2.name AS parent_symbol_name,
+ ps.section,
+ ps.type,
+ ps.synopsis,
+ m.version,
+ d.goos,
+ d.goarch
+ FROM modules m
+ INNER JOIN units u ON u.module_id = m.id
+ INNER JOIN documentation d ON d.unit_id = u.id
+ INNER JOIN documentation_symbols ds ON ds.documentation_id = d.id
+ INNER JOIN package_symbols ps ON ps.id = ds.package_symbol_id
+ INNER JOIN paths p1 ON u.path_id = p1.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
+ WHERE
+ p1.path = $1
+ AND m.module_path = $2
+ AND NOT m.incompatible
+ AND m.version_type = 'release'
+ ORDER BY
+ CASE WHEN ps.type='Type' THEN 0 ELSE 1 END,
+ symbol_name;`
+
+ // versionToNameToUnitSymbol contains all of the types for this unit,
+ // grouped by name and build context. This is used to keep track of the
+ // parent types, so that we can map the children to those symbols.
+ sh := internal.NewSymbolHistory()
+ collect := func(rows *sql.Rows) error {
+ var (
+ sm internal.SymbolMeta
+ build internal.BuildContext
+ v string
+ )
+ if err := rows.Scan(
+ &sm.Name,
+ &sm.ParentName,
+ &sm.Section,
+ &sm.Kind,
+ &sm.Synopsis,
+ &v,
+ &build.GOOS,
+ &build.GOARCH,
+ ); err != nil {
+ return fmt.Errorf("row.Scan(): %v", err)
+ }
+ if sm.Section == internal.SymbolSectionTypes && sm.Kind != internal.SymbolKindType {
+ if _, err := sh.GetSymbol(sm.ParentName, v, build); err != nil {
+ return err
+ }
+ }
+ sh.AddSymbol(sm, v, build)
+ return nil
+ }
+ if err := ddb.RunQuery(ctx, query, collect, packagePath, modulePath); err != nil {
+ return nil, err
+ }
+ return sh, nil
+}
+
// legacyGetPackageSymbols returns all of the symbols for a given package path and module path.
func legacyGetPackageSymbols(ctx context.Context, ddb *database.DB, packagePath, modulePath string,
) (_ map[string]map[string]*internal.UnitSymbol, err error) {
- defer derrors.Wrap(&err, "getPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
+ defer derrors.Wrap(&err, "legacyGetPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
defer middleware.ElapsedStat(ctx, "getPackageSymbols")()
query := `
SELECT
diff --git a/internal/postgres/symbol_history.go b/internal/postgres/symbol_history.go
index ab9df5f..24f0eae 100644
--- a/internal/postgres/symbol_history.go
+++ b/internal/postgres/symbol_history.go
@@ -18,6 +18,94 @@
"golang.org/x/pkgsite/internal/symbol"
)
+// GetSymbolHistory returns a SymbolHistory, which is a representation of the
+// first version when a symbol is added to an API.
+func (db *DB) GetSymbolHistory(ctx context.Context, packagePath, modulePath string,
+) (_ *internal.SymbolHistory, err error) {
+ defer derrors.Wrap(&err, "GetSymbolHistory(ctx, %q, %q)", packagePath, modulePath)
+ defer middleware.ElapsedStat(ctx, "GetSymbolHistory")()
+
+ if experiment.IsActive(ctx, internal.ExperimentReadSymbolHistory) {
+ return GetSymbolHistoryFromTable(ctx, db.db, packagePath, modulePath)
+ }
+ return GetSymbolHistoryWithPackageSymbols(ctx, db.db, packagePath, modulePath)
+}
+
+// GetSymbolHistoryFromTable returns a SymbolHistory, which is a representation of the
+// first version when a symbol is added to an API. It reads data from the
+// symbol_history table.
+func GetSymbolHistoryFromTable(ctx context.Context, ddb *database.DB,
+ packagePath, modulePath string) (_ *internal.SymbolHistory, err error) {
+ defer derrors.WrapStack(&err, "GetSymbolHistoryFromTable(ctx, ddb, %q, %q)", packagePath, modulePath)
+
+ q := squirrel.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").
+ Join("package_symbols ps ON ps.id = sh.package_symbol_id").
+ Join("symbol_names s1 ON ps.symbol_name_id = s1.id").
+ Join("symbol_names s2 ON ps.parent_symbol_name_id = s2.id").
+ Join("paths p1 ON sh.package_path_id = p1.id").
+ Join("paths p2 ON sh.module_path_id = p2.id").
+ Where(squirrel.Eq{"p1.path": packagePath}).
+ Where(squirrel.Eq{"p2.path": modulePath})
+ query, args, err := q.PlaceholderFormat(squirrel.Dollar).ToSql()
+ if err != nil {
+ return nil, err
+ }
+
+ // versionToNameToUnitSymbol is a map of the version a symbol was
+ // introduced, to the name and unit symbol.
+ sh := internal.NewSymbolHistory()
+ collect := func(rows *sql.Rows) error {
+ var (
+ sm internal.SymbolMeta
+ build internal.BuildContext
+ v string
+ )
+ if err := rows.Scan(
+ &sm.Name,
+ &sm.ParentName,
+ &sm.Section,
+ &sm.Kind,
+ &sm.Synopsis,
+ &v,
+ &build.GOOS,
+ &build.GOARCH,
+ ); err != nil {
+ return fmt.Errorf("row.Scan(): %v", err)
+ }
+ sh.AddSymbol(sm, v, build)
+ return nil
+ }
+ if err := ddb.RunQuery(ctx, query, collect, args...); err != nil {
+ return nil, err
+ }
+ return sh, nil
+}
+
+// GetSymbolHistoryWithPackageSymbols fetches symbol history data by using data
+// from package_symbols and documentation_symbols, and computed using
+// symbol.IntroducedHistory.
+//
+// GetSymbolHistoryWithPackageSymbols is exported for use in tests.
+func GetSymbolHistoryWithPackageSymbols(ctx context.Context, ddb *database.DB,
+ packagePath, modulePath string) (_ *internal.SymbolHistory, err error) {
+ defer derrors.WrapStack(&err, "GetSymbolHistoryWithPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
+ defer middleware.ElapsedStat(ctx, "GetSymbolHistoryWithPackageSymbols")()
+ sh, err := getPackageSymbols(ctx, ddb, packagePath, modulePath)
+ if err != nil {
+ return nil, err
+ }
+ return symbol.IntroducedHistory(sh)
+}
+
// LegacyGetSymbolHistory returns a map of the first version when a symbol name is
// added to the API, to the symbol name, to the UnitSymbol struct. The
// UnitSymbol.Children field will always be empty, as children names are also
diff --git a/internal/symbol.go b/internal/symbol.go
index 49583d7..eb9eac0 100644
--- a/internal/symbol.go
+++ b/internal/symbol.go
@@ -4,7 +4,13 @@
package internal
-import "sort"
+import (
+ "fmt"
+ "sort"
+
+ "golang.org/x/mod/semver"
+ "golang.org/x/pkgsite/internal/derrors"
+)
// SymbolSection is the documentation section where a symbol appears.
type SymbolSection string
@@ -47,6 +53,8 @@
GOARCH string
}
+// SymbolMeta is the metadata for an element in the package API. A symbol can
+// be a constant, variable, function, or type.
type SymbolMeta struct {
// Name is the name of the symbol.
Name string
@@ -68,6 +76,90 @@
ParentName string
}
+// SymbolHistory represents the history for when a symbol name was first added
+// to a package.
+type SymbolHistory struct {
+ // m is a map of version to name to SymbolMeta to UnitSymbol.
+ // SymbolMeta is stored as a distinct key from name, since it is possible
+ // for a symbol in the same version for different build contexts to have
+ // different SymbolMeta. For example:
+ // https://pkg.go.dev/syscall@go1.16.3#CloseOnExec has function signature:
+ // func CloseOnExec(fd int)
+ //
+ // versus
+ // https://pkg.go.dev/syscall?GOOS=windows#CloseOnExec has function
+ // signature:
+ // func CloseOnExec(fd Handle)
+ m map[string]map[string]map[SymbolMeta]*UnitSymbol
+}
+
+// NewSymbolHistory returns a new *SymbolHistory.
+func NewSymbolHistory() *SymbolHistory {
+ return &SymbolHistory{
+ m: map[string]map[string]map[SymbolMeta]*UnitSymbol{},
+ }
+}
+
+// SymbolsAtVersion returns a map of name to SymbolMeta to UnitSymbol for a
+// given version.
+func (sh *SymbolHistory) SymbolsAtVersion(v string) map[string]map[SymbolMeta]*UnitSymbol {
+ return sh.m[v]
+}
+
+// Versions returns an array of the versions in versionToNameToUnitSymbol, sorted by
+// increasing semver.
+func (sh *SymbolHistory) Versions() []string {
+ var orderdVersions []string
+ for v := range sh.m {
+ orderdVersions = append(orderdVersions, v)
+ }
+ sort.Slice(orderdVersions, func(i, j int) bool {
+ return semver.Compare(orderdVersions[i], orderdVersions[j]) == -1
+ })
+ return orderdVersions
+}
+
+// GetSymbol returns the unit symbol for a given name, version and build context.
+func (sh *SymbolHistory) GetSymbol(name, v string, build BuildContext) (_ *UnitSymbol, err error) {
+ defer derrors.Wrap(&err, "GetSymbol(%q, %q, %v)", name, v, build)
+ sav, ok := sh.m[v]
+ if !ok {
+ return nil, fmt.Errorf("version %q could not be found: %q", v, name)
+ }
+ stu, ok := sav[name]
+ if !ok {
+ return nil, fmt.Errorf("symbol %q could not be found at version %q", name, v)
+ }
+ for _, us := range stu {
+ if us.SupportsBuild(build) {
+ return us, nil
+ }
+ }
+ return nil, fmt.Errorf("symbol %q does not have build %v at version %q", name, build, v)
+}
+
+// AddSymbol adds the given symbol to SymbolHistory.
+func (sh *SymbolHistory) AddSymbol(sm SymbolMeta, v string, build BuildContext) {
+ sav, ok := sh.m[v]
+ if !ok {
+ sav = map[string]map[SymbolMeta]*UnitSymbol{}
+ sh.m[v] = sav
+ }
+ stu, ok := sav[sm.Name]
+ if !ok {
+ stu = map[SymbolMeta]*UnitSymbol{}
+ sh.m[v][sm.Name] = stu
+ }
+ us, ok := stu[sm]
+ if !ok {
+ us = &UnitSymbol{
+ SymbolMeta: sm,
+ }
+ sh.m[v][sm.Name][sm] = us
+ }
+ us.AddBuildContext(build)
+}
+
// UnitSymbol represents a symbol that is part of a unit.
type UnitSymbol struct {
SymbolMeta
diff --git a/internal/symbol/intro.go b/internal/symbol/intro.go
index ae31470..78988d9 100644
--- a/internal/symbol/intro.go
+++ b/internal/symbol/intro.go
@@ -9,8 +9,50 @@
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
)
+// IntroducedHistory returns a map of the first version when a symbol name is
+// added to the API, to the symbol name, to the UnitSymbol struct. The
+// UnitSymbol.Children field will always be empty, as children names are also
+// tracked.
+func IntroducedHistory(sh *internal.SymbolHistory) (outSH *internal.SymbolHistory, err error) {
+ defer derrors.Wrap(&err, "IntroducedHistory")
+ // Generate a map of the symbol names for each build context, and the first
+ // version when that symbol name was found in the package.
+ buildToNameToVersion := map[internal.BuildContext]map[string]string{}
+ for _, v := range sh.Versions() {
+ sv := sh.SymbolsAtVersion(v)
+ for _, su := range sv {
+ for sm, us := range su {
+ for _, build := range us.BuildContexts() {
+ if _, ok := buildToNameToVersion[build]; !ok {
+ buildToNameToVersion[build] = map[string]string{}
+ }
+ if _, ok := buildToNameToVersion[build][sm.Name]; !ok {
+ buildToNameToVersion[build][sm.Name] = v
+ }
+ }
+ }
+ }
+ }
+
+ // Using the map of buildToNameToVersion, construct a symbol history,
+ // where version is the first version when the symbol name was found in the
+ // package.
+ outSH = internal.NewSymbolHistory()
+ for build, nameToVersion := range buildToNameToVersion {
+ for name, version := range nameToVersion {
+ us, err := sh.GetSymbol(name, version, build)
+ if err != nil {
+ return nil, err
+ }
+ outSH.AddSymbol(us.SymbolMeta, version, build)
+ }
+ }
+ return outSH, nil
+}
+
// LegacyIntroducedHistory returns a map of the first version when a symbol name is
// added to the API, to the symbol name, to the UnitSymbol struct. The
// UnitSymbol.Children field will always be empty, as children names are also