drone/app/store/database/space_path.go

259 lines
7.8 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"context"
"fmt"
"github.com/harness/gitness/app/paths"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
)
var _ store.SpacePathStore = (*SpacePathStore)(nil)
// NewSpacePathStore returns a new SpacePathStore.
func NewSpacePathStore(
db *sqlx.DB,
pathTransformation store.SpacePathTransformation,
) *SpacePathStore {
return &SpacePathStore{
db: db,
spacePathTransformation: pathTransformation,
}
}
// SpacePathStore implements a store.SpacePathStore backed by a relational database.
type SpacePathStore struct {
db *sqlx.DB
spacePathTransformation store.SpacePathTransformation
}
// spacePathSegment is an internal representation of a segment of a space path.
type spacePathSegment struct {
ID int64 `db:"space_path_id"`
// Identifier is the original identifier that was provided
Identifier string `db:"space_path_uid"`
// IdentifierUnique is a transformed version of Identifier which is used to ensure uniqueness guarantees
IdentifierUnique string `db:"space_path_uid_unique"`
// IsPrimary indicates whether the path is the primary path of the space
// IMPORTANT: to allow DB enforcement of at most one primary path per repo/space
// we have a unique index on spaceID + IsPrimary and set IsPrimary to true
// for primary paths and to nil for non-primary paths.
IsPrimary null.Bool `db:"space_path_is_primary"`
ParentID null.Int `db:"space_path_parent_id"`
SpaceID int64 `db:"space_path_space_id"`
CreatedBy int64 `db:"space_path_created_by"`
Created int64 `db:"space_path_created"`
Updated int64 `db:"space_path_updated"`
}
const (
spacePathColumns = `
space_path_uid
,space_path_uid_unique
,space_path_is_primary
,space_path_parent_id
,space_path_space_id
,space_path_created_by
,space_path_created
,space_path_updated`
spacePathSelectBase = `
SELECT` + spacePathColumns + `
FROM space_paths`
)
// InsertSegment inserts a space path segment to the table - returns the full path.
func (s *SpacePathStore) InsertSegment(ctx context.Context, segment *types.SpacePathSegment) error {
const sqlQuery = `
INSERT INTO space_paths (
space_path_uid
,space_path_uid_unique
,space_path_is_primary
,space_path_parent_id
,space_path_space_id
,space_path_created_by
,space_path_created
,space_path_updated
) values (
:space_path_uid
,:space_path_uid_unique
,:space_path_is_primary
,:space_path_parent_id
,:space_path_space_id
,:space_path_created_by
,:space_path_created
,:space_path_updated
) RETURNING space_path_id`
db := dbtx.GetAccessor(ctx, s.db)
query, arg, err := db.BindNamed(sqlQuery, s.mapToInternalSpacePathSegment(segment))
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind path segment object")
}
if err = db.QueryRowContext(ctx, query, arg...).Scan(&segment.ID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Insert query failed")
}
return nil
}
func (s *SpacePathStore) FindPrimaryBySpaceID(ctx context.Context, spaceID int64) (*types.SpacePath, error) {
sqlQuery := spacePathSelectBase + `
where space_path_space_id = $1 AND space_path_is_primary = TRUE`
db := dbtx.GetAccessor(ctx, s.db)
dst := new(spacePathSegment)
path := ""
nextSpaceID := null.IntFrom(spaceID)
for nextSpaceID.Valid {
err := db.GetContext(ctx, dst, sqlQuery, nextSpaceID.Int64)
if err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find primary segment for %d", nextSpaceID.Int64)
}
path = paths.Concatenate(dst.Identifier, path)
nextSpaceID = dst.ParentID
}
return &types.SpacePath{
SpaceID: spaceID,
Value: path,
IsPrimary: true,
}, nil
}
func (s *SpacePathStore) FindByPath(ctx context.Context, path string) (*types.SpacePath, error) {
const sqlQueryNoParent = spacePathSelectBase + ` WHERE space_path_uid_unique = $1 AND space_path_parent_id IS NULL`
const sqlQueryParent = spacePathSelectBase + ` WHERE space_path_uid_unique = $1 AND space_path_parent_id = $2`
db := dbtx.GetAccessor(ctx, s.db)
segment := new(spacePathSegment)
segmentIdentifiers := paths.Segments(path)
if len(segmentIdentifiers) == 0 {
return nil, fmt.Errorf("path with no segments was passed '%s'", path)
}
var err error
var parentID int64
originalPath := ""
isPrimary := true
for i, segmentIdentifier := range segmentIdentifiers {
uniqueSegmentIdentifier := s.spacePathTransformation(segmentIdentifier, i == 0)
if parentID == 0 {
err = db.GetContext(ctx, segment, sqlQueryNoParent, uniqueSegmentIdentifier)
} else {
err = db.GetContext(ctx, segment, sqlQueryParent, uniqueSegmentIdentifier, parentID)
}
if err != nil {
return nil, database.ProcessSQLErrorf(
ctx,
err,
"Failed to find segment for '%s' in '%s'",
uniqueSegmentIdentifier,
path,
)
}
originalPath = paths.Concatenate(originalPath, segment.Identifier)
parentID = segment.SpaceID
isPrimary = isPrimary && segment.IsPrimary.ValueOrZero()
}
return &types.SpacePath{
Value: originalPath,
IsPrimary: isPrimary,
SpaceID: segment.SpaceID,
}, nil
}
// DeletePrimarySegment deletes the primary segment of the space.
func (s *SpacePathStore) DeletePrimarySegment(ctx context.Context, spaceID int64) error {
const sqlQuery = `
DELETE FROM space_paths
WHERE space_path_space_id = $1 AND space_path_is_primary = TRUE`
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, sqlQuery, spaceID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
}
return nil
}
// DeletePathsAndDescendandPaths deletes all space paths reachable from spaceID including itself.
func (s *SpacePathStore) DeletePathsAndDescendandPaths(ctx context.Context, spaceID int64) error {
const sqlQuery = `WITH RECURSIVE DescendantPaths AS (
SELECT space_path_id, space_path_space_id, space_path_parent_id
FROM space_paths
WHERE space_path_space_id = $1
UNION
SELECT sp.space_path_id, sp.space_path_space_id, sp.space_path_parent_id
FROM space_paths sp
JOIN DescendantPaths dp ON sp.space_path_parent_id = dp.space_path_space_id
)
DELETE FROM space_paths
WHERE space_path_id IN (SELECT space_path_id FROM DescendantPaths);`
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, sqlQuery, spaceID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
}
return nil
}
func (s *SpacePathStore) mapToInternalSpacePathSegment(p *types.SpacePathSegment) *spacePathSegment {
res := &spacePathSegment{
ID: p.ID,
Identifier: p.Identifier,
IdentifierUnique: s.spacePathTransformation(p.Identifier, p.ParentID == 0),
SpaceID: p.SpaceID,
Created: p.Created,
CreatedBy: p.CreatedBy,
Updated: p.Updated,
// ParentID: is set below
// IsPrimary: is set below
}
// 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)
}
if p.ParentID > 0 {
res.ParentID = null.IntFrom(p.ParentID)
}
return res
}