| // 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) | |
| 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 | |
| } |