mirror of
https://github.com/harness/drone.git
synced 2025-05-04 23:59:05 +08:00
501 lines
14 KiB
Go
501 lines
14 KiB
Go
// Copyright 2022 Harness Inc. All rights reserved.
|
|
// Use of this source code is governed by the Polyform Free Trial License
|
|
// that can be found in the LICENSE.md file for this repository.
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/internal/paths"
|
|
"github.com/harness/gitness/internal/store"
|
|
gitness_store "github.com/harness/gitness/store"
|
|
"github.com/harness/gitness/store/database"
|
|
"github.com/harness/gitness/store/database/dbtx"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/guregu/null"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var _ store.PathStore = (*PathStore)(nil)
|
|
|
|
// NewSpaceStore returns a new PathStore.
|
|
func NewPathStore(db *sqlx.DB, pathTransformation store.PathTransformation) *PathStore {
|
|
return &PathStore{
|
|
db: db,
|
|
pathTransformation: pathTransformation,
|
|
}
|
|
}
|
|
|
|
// PathStore implements a store.PathStore backed by a relational database.
|
|
type PathStore struct {
|
|
db *sqlx.DB
|
|
pathTransformation store.PathTransformation
|
|
}
|
|
|
|
// path is an internal representation used to store path data in DB.
|
|
type path struct {
|
|
ID int64 `db:"path_id"`
|
|
Version int64 `db:"path_version"`
|
|
// Value is the original path that was provided
|
|
Value string `db:"path_value"`
|
|
// ValueUnique is a transformed version of Value which is used to ensure uniqueness guarantees
|
|
ValueUnique string `db:"path_value_unique"`
|
|
// IsPrimary indicates whether the path is the primary path of the repo/space
|
|
// IMPORTANT: to allow DB enforcement of at most one primary path per repo/space
|
|
// we have a unique index on repoID|spaceID + IsPrimary and set IsPrimary to true
|
|
// for primary paths and to nil for non-primary paths.
|
|
IsPrimary null.Bool `db:"path_is_primary"`
|
|
SpaceID null.Int `db:"path_space_id"`
|
|
RepoID null.Int `db:"path_repo_id"`
|
|
CreatedBy int64 `db:"path_created_by"`
|
|
Created int64 `db:"path_created"`
|
|
Updated int64 `db:"path_updated"`
|
|
}
|
|
|
|
const (
|
|
pathColumns = `
|
|
path_id
|
|
,path_version
|
|
,path_value
|
|
,path_value_unique
|
|
,path_is_primary
|
|
,path_space_id
|
|
,path_repo_id
|
|
,path_created_by
|
|
,path_created
|
|
,path_updated`
|
|
|
|
pathSelectBase = `
|
|
SELECT` + pathColumns + `
|
|
FROM paths`
|
|
)
|
|
|
|
// Create creates a new path.
|
|
func (s *PathStore) Create(ctx context.Context, path *types.Path) error {
|
|
const sqlQuery = `
|
|
INSERT INTO paths (
|
|
path_version
|
|
,path_value
|
|
,path_value_unique
|
|
,path_is_primary
|
|
,path_space_id
|
|
,path_repo_id
|
|
,path_created_by
|
|
,path_created
|
|
,path_updated
|
|
) values (
|
|
:path_version
|
|
,:path_value
|
|
,:path_value_unique
|
|
,:path_is_primary
|
|
,:path_space_id
|
|
,:path_repo_id
|
|
,:path_created_by
|
|
,:path_created
|
|
,:path_updated
|
|
) RETURNING path_id`
|
|
|
|
// map to internal path
|
|
dbPath, err := s.mapToInternalPath(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to map path: %w", err)
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
query, arg, err := db.BindNamed(sqlQuery, dbPath)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
if err = db.QueryRowContext(ctx, query, arg...).Scan(&path.ID); err != nil {
|
|
return database.ProcessSQLErrorf(err, "Insert query failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findInner finds the path for the given id and locks the record if requested.
|
|
func (s *PathStore) findInner(ctx context.Context, id int64, lock bool) (*types.Path, error) {
|
|
sqlQuery := pathSelectBase + `
|
|
WHERE path_id = $1`
|
|
|
|
if lock && !strings.HasPrefix(s.db.DriverName(), "sqlite") {
|
|
sqlQuery += "\n" + database.SQLForUpdate
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := new(path)
|
|
err := db.GetContext(ctx, dst, sqlQuery, id)
|
|
if err != nil {
|
|
return nil, database.ProcessSQLErrorf(err, "Failed to find path")
|
|
}
|
|
|
|
res, err := mapToPath(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map path to external type: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Find finds the path for the given id.
|
|
func (s *PathStore) Find(ctx context.Context, id int64) (*types.Path, error) {
|
|
return s.findInner(ctx, id, false)
|
|
}
|
|
|
|
// FindWithLock finds the path for the given id and locks the entry.
|
|
func (s *PathStore) FindWithLock(ctx context.Context, id int64) (*types.Path, error) {
|
|
return s.findInner(ctx, id, true)
|
|
}
|
|
|
|
// FindValue finds the path for the given value.
|
|
func (s *PathStore) FindValue(ctx context.Context, value string) (*types.Path, error) {
|
|
const sqlQuery = pathSelectBase + `
|
|
WHERE path_value_unique = $1`
|
|
|
|
// map the Value to unique Value before searching!
|
|
valueUnique, err := s.pathTransformation(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform path '%s': %w", value, err)
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := new(path)
|
|
err = db.GetContext(ctx, dst, sqlQuery, valueUnique)
|
|
if err != nil {
|
|
return nil, database.ProcessSQLErrorf(err, "Failed to find path")
|
|
}
|
|
|
|
res, err := mapToPath(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map path to external type: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// findPrimaryInternal finds the primary path for the given target and locks the record if requested.
|
|
func (s *PathStore) findPrimaryInternal(ctx context.Context,
|
|
targetType enum.PathTargetType, targetID int64, lock bool) (*types.Path, error) {
|
|
stmt := database.Builder.
|
|
Select(pathColumns).
|
|
From("paths").
|
|
Where("path_is_primary = ?", true)
|
|
|
|
stmt, err := wherePathTarget(stmt, targetType, targetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if lock && !strings.HasPrefix(s.db.DriverName(), "sqlite") {
|
|
stmt.Suffix(database.SQLForUpdate)
|
|
}
|
|
|
|
sqlQuery, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert query to sql: %w", err)
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
// NOTE: there is at most one primary path (so no list needed)
|
|
dst := new(path)
|
|
err = db.GetContext(ctx, dst, sqlQuery, args...)
|
|
if err != nil {
|
|
return nil, database.ProcessSQLErrorf(err, "Failed to find path")
|
|
}
|
|
|
|
res, err := mapToPath(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map path to external type: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// FindPrimary finds the primary path for a target.
|
|
func (s *PathStore) FindPrimary(ctx context.Context,
|
|
targetType enum.PathTargetType, targetID int64) (*types.Path, error) {
|
|
return s.findPrimaryInternal(ctx, targetType, targetID, false)
|
|
}
|
|
|
|
// FindPrimaryWithLock finds the primary path for a target and locks the db entry.
|
|
func (s *PathStore) FindPrimaryWithLock(ctx context.Context,
|
|
targetType enum.PathTargetType, targetID int64) (*types.Path, error) {
|
|
return s.findPrimaryInternal(ctx, targetType, targetID, true)
|
|
}
|
|
|
|
// Update updates an existing path.
|
|
func (s *PathStore) Update(ctx context.Context, path *types.Path) error {
|
|
const sqlQuery = `
|
|
UPDATE paths
|
|
SET
|
|
path_version = :path_version
|
|
,path_updated = :path_updated
|
|
,path_value = :path_value
|
|
,path_value_unique = :path_value_unique
|
|
,path_is_primary = :path_is_primary
|
|
WHERE path_id = :path_id and path_version = :path_version - 1`
|
|
|
|
dbPath, err := s.mapToInternalPath(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to map path: %w", err)
|
|
}
|
|
|
|
dbPath.Version++
|
|
dbPath.Updated = time.Now().UnixMilli()
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
query, args, err := db.BindNamed(sqlQuery, dbPath)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
result, err := db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(err, "failed to update path")
|
|
}
|
|
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(err, "Failed to get number of updated rows")
|
|
}
|
|
|
|
if count == 0 {
|
|
return gitness_store.ErrVersionConflict
|
|
}
|
|
|
|
path.Version = dbPath.Version
|
|
path.Updated = dbPath.Updated
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes a specific path.
|
|
func (s *PathStore) Delete(ctx context.Context, id int64) error {
|
|
const sqlQuery = `
|
|
DELETE FROM paths
|
|
WHERE path_id = $1`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
if _, err := db.ExecContext(ctx, sqlQuery, id); err != nil {
|
|
return database.ProcessSQLErrorf(err, "Delete query failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Count returns the count of paths for a target.
|
|
func (s *PathStore) Count(ctx context.Context, targetType enum.PathTargetType, targetID int64,
|
|
opts *types.PathFilter) (int64, error) {
|
|
stmt := database.Builder.
|
|
Select("count(*)").
|
|
From("paths")
|
|
|
|
stmt, err := wherePathTarget(stmt, targetType, targetID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
sqlQuery, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to convert query to sql: %w", err)
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
var count int64
|
|
err = db.QueryRowContext(ctx, sqlQuery, args...).Scan(&count)
|
|
if err != nil {
|
|
return 0, database.ProcessSQLErrorf(err, "Failed executing count query")
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// List lists all paths for a target.
|
|
func (s *PathStore) List(ctx context.Context, targetType enum.PathTargetType, targetID int64,
|
|
opts *types.PathFilter) ([]*types.Path, error) {
|
|
// else we construct the sql statement.
|
|
stmt := database.Builder.
|
|
Select("*").
|
|
From("paths")
|
|
|
|
stmt, err := wherePathTarget(stmt, targetType, targetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stmt = stmt.Limit(database.Limit(opts.Size))
|
|
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
|
|
|
|
switch opts.Sort {
|
|
case enum.PathAttrID, enum.PathAttrNone:
|
|
// NOTE: string concatenation is safe because the
|
|
// order attribute is an enum and is not user-defined,
|
|
// and is therefore not subject to injection attacks.
|
|
stmt = stmt.OrderBy("path_id " + opts.Order.String())
|
|
case enum.PathAttrValue:
|
|
stmt = stmt.OrderBy("path_value " + opts.Order.String())
|
|
case enum.PathAttrCreated:
|
|
stmt = stmt.OrderBy("path_created " + opts.Order.String())
|
|
case enum.PathAttrUpdated:
|
|
stmt = stmt.OrderBy("path_updated " + opts.Order.String())
|
|
}
|
|
|
|
sqlQuery, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to convert query to sql")
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := []*path{}
|
|
if err = db.SelectContext(ctx, &dst, sqlQuery, args...); err != nil {
|
|
return nil, database.ProcessSQLErrorf(err, "Path select query failed")
|
|
}
|
|
|
|
res, err := mapToPaths(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map paths to external type: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// ListPrimaryDescendantsWithLock lists all primary paths that are descendants of the given path and locks them.
|
|
func (s *PathStore) ListPrimaryDescendantsWithLock(ctx context.Context, value string) ([]*types.Path, error) {
|
|
var sqlQuery = pathSelectBase + `
|
|
WHERE path_value_unique LIKE $1 AND path_is_primary = true`
|
|
|
|
if !strings.HasPrefix(s.db.DriverName(), "sqlite") {
|
|
sqlQuery += "\n" + database.SQLForUpdate
|
|
}
|
|
|
|
// map the Value to unique Value before searching!
|
|
valueUnique, err := s.pathTransformation(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform path '%s': %w", value, err)
|
|
}
|
|
// prepare input for LIKE query (space1/space2 -> space1/space2/%)
|
|
valueUniquePattern := paths.Concatinate(valueUnique, "%")
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := []*path{}
|
|
if err = db.SelectContext(ctx, &dst, sqlQuery, valueUniquePattern); err != nil {
|
|
return nil, database.ProcessSQLErrorf(err, "Failed to list paths")
|
|
}
|
|
|
|
res, err := mapToPaths(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map paths to external type: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// wherePathTarget adds a where statement for a path select statement filtering for the provided target.
|
|
func wherePathTarget(stmt squirrel.SelectBuilder,
|
|
targetType enum.PathTargetType, targetID int64) (squirrel.SelectBuilder, error) {
|
|
switch targetType {
|
|
case enum.PathTargetTypeRepo:
|
|
return stmt.Where("path_repo_id = ?", targetID), nil
|
|
case enum.PathTargetTypeSpace:
|
|
return stmt.Where("path_space_id = ?", targetID), nil
|
|
default:
|
|
return squirrel.SelectBuilder{}, fmt.Errorf("path target type '%s' is not supported", targetType)
|
|
}
|
|
}
|
|
|
|
func mapToPath(p *path) (*types.Path, error) {
|
|
res := &types.Path{
|
|
ID: p.ID,
|
|
Version: p.Version,
|
|
Value: p.Value,
|
|
Created: p.Created,
|
|
CreatedBy: p.CreatedBy,
|
|
Updated: p.Updated,
|
|
}
|
|
|
|
if p.IsPrimary.Valid {
|
|
res.IsPrimary = p.IsPrimary.Bool
|
|
}
|
|
|
|
switch {
|
|
case p.RepoID.Valid && p.SpaceID.Valid:
|
|
return nil, fmt.Errorf("both repoID and spaceID are set for path %d", p.ID)
|
|
case p.RepoID.Valid:
|
|
res.TargetType = enum.PathTargetTypeRepo
|
|
res.TargetID = p.RepoID.Int64
|
|
case p.SpaceID.Valid:
|
|
res.TargetType = enum.PathTargetTypeSpace
|
|
res.TargetID = p.SpaceID.Int64
|
|
default:
|
|
return nil, fmt.Errorf("neither repoID nor spaceID are set for path %d", p.ID)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func mapToPaths(paths []*path) ([]*types.Path, error) {
|
|
var err error
|
|
res := make([]*types.Path, len(paths))
|
|
for i := range paths {
|
|
res[i], err = mapToPath(paths[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *PathStore) mapToInternalPath(p *types.Path) (*path, error) {
|
|
// path comes from outside.
|
|
if p == nil {
|
|
return nil, fmt.Errorf("path is nil")
|
|
}
|
|
|
|
res := &path{
|
|
ID: p.ID,
|
|
Version: p.Version,
|
|
Value: p.Value,
|
|
Created: p.Created,
|
|
CreatedBy: p.CreatedBy,
|
|
Updated: p.Updated,
|
|
}
|
|
|
|
var err error
|
|
if res.ValueUnique, err = s.pathTransformation(p.Value); err != nil {
|
|
return nil, fmt.Errorf("failed to transform path: %w", err)
|
|
}
|
|
|
|
// only set IsPrimary to a value if it's true (Unique Index doesn't allow multiple false, hence keep it nil)
|
|
if p.IsPrimary {
|
|
res.IsPrimary = null.BoolFrom(true)
|
|
}
|
|
|
|
switch p.TargetType {
|
|
case enum.PathTargetTypeRepo:
|
|
res.RepoID = null.IntFrom(p.TargetID)
|
|
case enum.PathTargetTypeSpace:
|
|
res.SpaceID = null.IntFrom(p.TargetID)
|
|
default:
|
|
return nil, fmt.Errorf("path target type '%s' is not supported", p.TargetType)
|
|
}
|
|
|
|
return res, nil
|
|
}
|