blob: 9b2edf3b7070afa335b3cdaf711d80da96cdad42 [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"
"errors"
"fmt"
"sort"
"strings"
"github.com/lib/pq"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/database"
"golang.org/x/pkgsite/internal/derrors"
)
// GetLatestMajorPathForV1Path reports the latest unit path in the series for
// the given v1path. It also returns the major version for that path.
func (db *DB) GetLatestMajorPathForV1Path(ctx context.Context, v1path string) (_ string, _ int, err error) {
defer derrors.WrapStack(&err, "DB.GetLatestPathForV1Path(ctx, %q)", v1path)
q := `
SELECT p.path, m.series_path
FROM paths p
INNER JOIN units u ON u.path_id = p.id
INNER JOIN modules m ON u.module_id = m.id
WHERE u.v1path_id = (
SELECT p.id
FROM paths p
INNER JOIN units u ON u.v1path_id = p.id
WHERE p.path = $1
ORDER BY p.path DESC
LIMIT 1
);`
paths := map[string]string{} // from unit path to series path
err = db.db.RunQuery(ctx, q, func(rows *sql.Rows) error {
var p, sp string
if err := rows.Scan(&p, &sp); err != nil {
return err
}
paths[p] = sp
return nil
}, v1path)
if err != nil {
return "", 0, err
}
var (
maj int
majPath string
)
for p, sp := range paths {
// Trim the series path and suffix from the unit path.
// Keep only the N following vN.
suffix := internal.Suffix(v1path, sp)
modPath := strings.TrimSuffix(p, "/"+suffix)
_, i := internal.SeriesPathAndMajorVersion(modPath)
if i == 0 {
return "", 0, fmt.Errorf("bad module path %q", modPath)
}
if maj <= i {
maj = i
majPath = p
}
}
if maj == 0 {
// Return 1 as the major version for all v0 or v1 majPaths.
maj = 1
}
return majPath, maj, nil
}
// upsertPath adds path into the paths table if it does not exist, and returns
// its ID either way.
// It assumes it is running inside a transaction.
func upsertPath(ctx context.Context, tx *database.DB, path string) (id int, err error) {
// Doing the select first and then the insert led to uniqueness constraint
// violations even with fully serializable transactions; see
// https://www.postgresql.org/message-id/CAOqyxwL4E_JmUScYrnwd0_sOtm3bt4c7G%2B%2BUiD2PnmdGJFiqyQ%40mail.gmail.com.
// If the upsert is done first and then the select, then everything works
// fine.
defer derrors.WrapStack(&err, "upsertPath(%q)", path)
if _, err := tx.Exec(ctx, `LOCK TABLE paths IN EXCLUSIVE MODE`); err != nil {
return 0, err
}
err = tx.QueryRow(ctx,
`INSERT INTO paths (path) VALUES ($1) ON CONFLICT DO NOTHING RETURNING id`,
path).Scan(&id)
if err == sql.ErrNoRows {
err = tx.QueryRow(ctx,
`SELECT id FROM paths WHERE path = $1`,
path).Scan(&id)
if err == sql.ErrNoRows {
return 0, errors.New("got no rows; shouldn't happen")
}
}
if err != nil {
return 0, err
}
if id == 0 {
return 0, errors.New("zero ID")
}
return id, nil
}
// upsertPaths adds all the paths to the paths table if they aren't already
// there, and returns their ID either way.
// It assumes it is running inside a transaction.
func upsertPaths(ctx context.Context, db *database.DB, paths []string) (pathToID map[string]int, err error) {
defer derrors.WrapStack(&err, "upsertPaths(%d paths)", len(paths))
// Read all existing paths for this module, to avoid a large bulk upsert.
// (We've seen these bulk upserts hang for so long that they time out (10
// minutes)).
pathToID = map[string]int{}
collect := func(rows *sql.Rows) error {
var (
pathID int
path string
)
if err := rows.Scan(&pathID, &path); err != nil {
return err
}
pathToID[path] = pathID
return nil
}
if err := db.RunQuery(ctx, `SELECT id, path FROM paths WHERE path = ANY($1)`,
collect, pq.Array(paths)); err != nil {
return nil, err
}
// Insert any unit paths that we don't already have.
var values []any
for _, v := range paths {
if _, ok := pathToID[v]; !ok {
values = append(values, v)
}
}
if len(values) > 0 {
// Sort to avoid deadlock.
sort.Slice(values, func(i, j int) bool { return values[i].(string) < values[j].(string) })
// Insert data into the paths table.
pathCols := []string{"path"}
returningPathCols := []string{"id", "path"}
if err := db.BulkInsertReturning(ctx, "paths", pathCols, values,
database.OnConflictDoNothing, returningPathCols, collect); err != nil {
return nil, err
}
}
return pathToID, nil
}
func GetPathID(ctx context.Context, ddb *database.DB, path string) (id int, err error) {
err = ddb.QueryRow(ctx,
`SELECT id FROM paths WHERE path = $1`,
path).Scan(&id)
return id, err
}