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