blob: 70f815cdb67d42b35824b52f6d546ac1d7de512d [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/Masterminds/squirrel"
"github.com/lib/pq"
"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/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 := packageSymbolQueryJoin(
squirrel.Select(
"s1.name AS symbol_name",
"s2.name AS parent_symbol_name",
"ps.section",
"ps.type",
"ps.synopsis",
"m.version",
"d.goos",
"d.goarch"), packagePath, modulePath).
OrderBy("CASE WHEN ps.type='Type' THEN 0 ELSE 1 END").
OrderBy("s1.name")
q, args, err := query.PlaceholderFormat(squirrel.Dollar).ToSql()
if err != nil {
return nil, err
}
sh, collect := collectSymbolHistory(func(sh *internal.SymbolHistory, sm internal.SymbolMeta, v string, build internal.BuildContext) error {
if sm.Section == internal.SymbolSectionTypes && sm.Kind != internal.SymbolKindType {
_, err := sh.GetSymbol(sm.ParentName, v, build)
if err != nil {
return fmt.Errorf("could not find parent for %q: %v", sm.Name, err)
}
return nil
}
return nil
})
if err := ddb.RunQuery(ctx, q, collect, args...); err != nil {
return nil, err
}
return sh, nil
}
func packageSymbolQueryJoin(query squirrel.SelectBuilder, pkgPath, modulePath string) squirrel.SelectBuilder {
return query.From("modules m").
Join("units u on u.module_id = m.id").
Join("documentation d ON d.unit_id = u.id").
Join("documentation_symbols ds ON ds.documentation_id = d.id").
Join("package_symbols ps ON ps.id = ds.package_symbol_id").
Join("paths p1 ON u.path_id = p1.id").
Join("symbol_names s1 ON ps.symbol_name_id = s1.id").
Join("symbol_names s2 ON ps.parent_symbol_name_id = s2.id").
Where(squirrel.Eq{"p1.path": pkgPath}).
Where(squirrel.Eq{"m.module_path": modulePath}).
Where("NOT m.incompatible").
Where(squirrel.Eq{"m.version_type": "release"})
}
func collectSymbolHistory(check func(sh *internal.SymbolHistory, sm internal.SymbolMeta, v string, build internal.BuildContext) error) (*internal.SymbolHistory, func(rows *sql.Rows) error) {
sh := internal.NewSymbolHistory()
return sh, func(rows *sql.Rows) (err error) {
defer derrors.Wrap(&err, "collectSymbolHistory")
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 err := check(sh, sm, v, build); err != nil {
return fmt.Errorf("check(): %v", err)
}
sh.AddSymbol(sm, v, build)
return nil
}
}
func upsertSearchDocumentSymbols(ctx context.Context, ddb *database.DB,
packagePath, modulePath, v string) (err error) {
defer derrors.Wrap(&err, "upsertSearchDocumentSymbols(ctx, ddb, %q, %q, %q)", packagePath, modulePath, v)
defer middleware.ElapsedStat(ctx, "upsertSearchDocumentSymbols")()
if !experiment.IsActive(ctx, internal.ExperimentInsertSymbolSearchDocuments) {
return nil
}
// If a user is looking for the symbol "DB.Begin", from package
// database/sql, we want them to be able to find this by searching for
// "DB.Begin" and "sql.DB.Begin". Searching for "sql.DB", "DB", "Begin" or
// "sql.DB" will not return "DB.Begin".
query := packageSymbolQueryJoin(squirrel.Select(
"p1.id AS package_path_id",
"s1.id AS symbol_name_id",
// Group the build contexts as an array, with the format
// "<goos>/<goarch>". We only care about the build contexts when the
// default goos/goarch for the package page does not contain the
// matching symbol.
//
// TODO(https://golang/issue/44142): We could probably get away with
// storing just the GOOS value, since we don't really need the GOARCH
// to link to a symbol page. If we do that we should also change the
// column type to []goos.
//
// Store in order of the build context list at internal.BuildContexts.
`ARRAY_AGG(FORMAT('%s/%s', d.goos, d.goarch)
ORDER BY
CASE WHEN d.goos='linux' THEN 0
WHEN d.goos='windows' THEN 1
WHEN d.goos='darwin' THEN 2
WHEN d.goos='js' THEN 3 END)`,
// If a user is looking for the symbol "DB.Begin", from package
// database/sql, we want them to be able to find this by searching for
// "DB.Begin", "Begin", and "sql.DB.Begin". Searching for "sql.DB" or
// "DB" will not return "DB.Begin".
//
// Index <package>.<identifier> (i.e. "sql.DB.Begin")
`SETWEIGHT(
TO_TSVECTOR('simple', concat(s1.name, ' ', concat(u.name, '.', s1.name))),
'A') ||`+
// Index <identifier>, including the parent name (i.e. DB.Begin).
`SETWEIGHT(
TO_TSVECTOR('simple', s1.name),
'A') ||`+
// Index <identifier> without parent name (i.e. "Begin").
//
// This is weighted less, so that if other symbols are just named
// "Begin" they will rank higher in a search for "Begin".
`SETWEIGHT(
TO_TSVECTOR('simple', split_part(s1.name, '.', 2)),
'B') AS tokens`,
), packagePath, modulePath).
Where(squirrel.Eq{"m.version": v}).
GroupBy("p1.id, s1.id", "tokens").
OrderBy("s1.name")
q, args, err := query.PlaceholderFormat(squirrel.Dollar).ToSql()
if err != nil {
return err
}
var values []interface{}
collect := func(rows *sql.Rows) (err error) {
var (
packagePathID int
symbolNameID int
tokens string
buildContexts []string
)
if err := rows.Scan(
&packagePathID,
&symbolNameID,
pq.Array(&buildContexts),
&tokens,
); err != nil {
return fmt.Errorf("row.Scan(): %v", err)
}
values = append(values, packagePathID, symbolNameID, pq.Array(buildContexts), tokens)
return nil
}
if err := ddb.RunQuery(ctx, q, collect, args...); err != nil {
return err
}
columns := []string{"package_path_id", "symbol_name_id", "build_contexts", "tsv_symbol_tokens"}
return ddb.BulkInsert(ctx, "symbol_search_documents", columns, values,
`ON CONFLICT (package_path_id, symbol_name_id)
DO UPDATE
SET
build_contexts=excluded.build_contexts,
tsv_symbol_tokens=excluded.tsv_symbol_tokens`)
}