drone/internal/store/database/path.go
2023-06-20 00:26:40 +00:00

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
}