internal/postgres: handle multiple documentations for a package

Insert and retrieve documentation for all of a package's build
contexts.

For golang/go#37232

Change-Id: Ib3c7b32be66502c3f343bddd247de700e390c53d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/288831
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/postgres/insert_module.go b/internal/postgres/insert_module.go
index 3ee2374..5384343 100644
--- a/internal/postgres/insert_module.go
+++ b/internal/postgres/insert_module.go
@@ -292,7 +292,7 @@
 		paths         []string
 		unitValues    []interface{}
 		pathToReadme  = map[string]*internal.Readme{}
-		pathToDoc     = map[string]*internal.Documentation{}
+		pathToDoc     = map[string][]*internal.Documentation{}
 		pathToImports = map[string][]string{}
 		pathIDToPath  = map[int]string{}
 	)
@@ -331,14 +331,12 @@
 		if u.Readme != nil {
 			pathToReadme[u.Path] = u.Readme
 		}
-		if u.Documentation != nil && u.Documentation[0] != nil && u.Documentation[0].Source == nil {
-			return fmt.Errorf("insertUnits: unit %q missing source files", u.Path)
+		for _, d := range u.Documentation {
+			if d.Source == nil {
+				return fmt.Errorf("insertUnits: unit %q missing source files for %q, %q", u.Path, d.GOOS, d.GOARCH)
+			}
 		}
-		if u.Documentation == nil {
-			pathToDoc[u.Path] = nil
-		} else {
-			pathToDoc[u.Path] = u.Documentation[0]
-		}
+		pathToDoc[u.Path] = u.Documentation
 		if len(u.Imports) > 0 {
 			pathToImports[u.Path] = u.Imports
 		}
@@ -463,20 +461,18 @@
 func insertDoc(ctx context.Context, db *database.DB,
 	paths []string,
 	pathToUnitID map[string]int,
-	pathToDoc map[string]*internal.Documentation) (err error) {
+	pathToDoc map[string][]*internal.Documentation) (err error) {
 	defer derrors.Wrap(&err, "insertDoc")
 
 	var docValues []interface{}
 	for _, path := range paths {
-		doc := pathToDoc[path]
-		if doc == nil {
-			continue
-		}
 		unitID := pathToUnitID[path]
-		if doc.GOOS == "" || doc.GOARCH == "" {
-			return errors.New("empty GOOS or GOARCH")
+		for _, doc := range pathToDoc[path] {
+			if doc.GOOS == "" || doc.GOARCH == "" {
+				return errors.New("empty GOOS or GOARCH")
+			}
+			docValues = append(docValues, unitID, doc.GOOS, doc.GOARCH, doc.Synopsis, doc.Source)
 		}
-		docValues = append(docValues, unitID, doc.GOOS, doc.GOARCH, doc.Synopsis, doc.Source)
 	}
 	uniqueCols := []string{"unit_id", "goos", "goarch"}
 	docCols := append(uniqueCols, "synopsis", "source")
diff --git a/internal/postgres/unit.go b/internal/postgres/unit.go
index 97044f5..a18c59d 100644
--- a/internal/postgres/unit.go
+++ b/internal/postgres/unit.go
@@ -345,12 +345,10 @@
 	defer derrors.Wrap(&err, "getUnitWithAllFields(ctx, %q, %q, %q)", um.Path, um.ModulePath, um.Version)
 	defer middleware.ElapsedStat(ctx, "getUnitWithAllFields")()
 
+	// Get README and import counts.
 	query := `
         SELECT
-			d.goos,
-			d.goarch,
-			d.synopsis,
-			d.source,
+			u.id,
 			r.file_path,
 			r.contents,
 			COALESCE((
@@ -371,8 +369,6 @@
 		ON p.id = u.path_id
 		INNER JOIN modules m
 		ON u.module_id = m.id
-		LEFT JOIN documentation d
-		ON d.unit_id = u.id
 		LEFT JOIN readmes r
 		ON r.unit_id = u.id
 		WHERE
@@ -381,15 +377,12 @@
 			AND m.version = $3;`
 
 	var (
-		d internal.Documentation
-		r internal.Readme
-		u internal.Unit
+		unitID int
+		r      internal.Readme
+		u      internal.Unit
 	)
 	err = db.db.QueryRow(ctx, query, um.Path, um.ModulePath, um.Version).Scan(
-		database.NullIsEmpty(&d.GOOS),
-		database.NullIsEmpty(&d.GOARCH),
-		database.NullIsEmpty(&d.Synopsis),
-		&d.Source,
+		&unitID,
 		database.NullIsEmpty(&r.Filepath),
 		database.NullIsEmpty(&r.Contents),
 		&u.NumImports,
@@ -399,15 +392,38 @@
 	case sql.ErrNoRows:
 		return nil, derrors.NotFound
 	case nil:
-		if d.GOOS != "" {
-			u.Documentation = []*internal.Documentation{&d}
-		}
 		if r.Filepath != "" {
 			u.Readme = &r
 		}
 	default:
 		return nil, err
 	}
+
+	// Get documentation. There can be multiple rows.
+	query = `
+		SELECT goos, goarch, synopsis, source
+		FROM documentation
+		WHERE unit_id = $1
+	`
+	err = db.db.RunQuery(ctx, query, func(rows *sql.Rows) error {
+		var d internal.Documentation
+		if err := rows.Scan(&d.GOOS, &d.GOARCH, &d.Synopsis, &d.Source); err != nil {
+			return err
+		}
+		u.Documentation = append(u.Documentation, &d)
+		return nil
+	}, unitID)
+	if err != nil {
+		return nil, err
+	}
+	// Sort documentation by GOOS/GOARCH.
+	sort.Slice(u.Documentation, func(i, j int) bool {
+		ci := u.Documentation[i].BuildContext()
+		cj := u.Documentation[j].BuildContext()
+		return internal.CompareBuildContexts(ci, cj) < 0
+	})
+
+	// Get other info.
 	pkgs, err := db.getPackagesInUnit(ctx, um.Path, um.ModulePath, um.Version)
 	if err != nil {
 		return nil, err
diff --git a/internal/postgres/unit_test.go b/internal/postgres/unit_test.go
index 9a2a73c..a3daca9 100644
--- a/internal/postgres/unit_test.go
+++ b/internal/postgres/unit_test.go
@@ -446,6 +446,20 @@
 		t.Fatal(err)
 	}
 
+	// Add a module that has documentation for two Go build contexts.
+	m = sample.Module("a.com/twodoc", "v1.2.3", "p")
+	pkg := m.Packages()[0]
+	doc2 := &internal.Documentation{
+		GOOS:     "windows",
+		GOARCH:   "amd64",
+		Synopsis: pkg.Documentation[0].Synopsis + " 2",
+		Source:   pkg.Documentation[0].Source,
+	}
+	pkg.Documentation = append(pkg.Documentation, doc2)
+	if err := testDB.InsertModule(ctx, m); err != nil {
+		t.Fatal(err)
+	}
+
 	for _, test := range []struct {
 		name, path, modulePath, version string
 		want                            *internal.Unit
@@ -555,6 +569,19 @@
 				},
 			),
 		},
+		{
+			name:       "package with two docs",
+			path:       "a.com/twodoc/p",
+			modulePath: "a.com/twodoc",
+			version:    "v1.2.3",
+			want: func() *internal.Unit {
+				u := unit("a.com/twodoc/p", "a.com/twodoc", "v1.2.3", "p",
+					nil,
+					[]string{"p"})
+				u.Documentation = append(u.Documentation, doc2)
+				return u
+			}(),
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			um := sample.UnitMeta(