// 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/internal/paths" "github.com/harness/gitness/internal/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"` // UID is the original uid that was provided UID string `db:"space_path_uid"` // UIDUnique is a transformed version of UID which is used to ensure uniqueness guarantees UIDUnique 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 )` db := dbtx.GetAccessor(ctx, s.db) query, arg, err := db.BindNamed(sqlQuery, s.mapToInternalSpacePathSegment(segment)) if err != nil { return database.ProcessSQLErrorf(err, "Failed to bind path segment object") } if _, err = db.ExecContext(ctx, query, arg...); err != nil { return database.ProcessSQLErrorf(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(err, "Failed to find primary segment for %d", nextSpaceID.Int64) } path = paths.Concatinate(dst.UID, 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) segmentUIDs := paths.Segments(path) if len(segmentUIDs) == 0 { return nil, fmt.Errorf("path with no segments was passed '%s'", path) } var err error var parentID int64 originalPath := "" isPrimary := true for i, segmentUID := range segmentUIDs { uniqueSegmentUID := s.spacePathTransformation(segmentUID, i == 0) if parentID == 0 { err = db.GetContext(ctx, segment, sqlQueryNoParent, uniqueSegmentUID) } else { err = db.GetContext(ctx, segment, sqlQueryParent, uniqueSegmentUID, parentID) } if err != nil { return nil, database.ProcessSQLErrorf(err, "Failed to find segment for '%s' in '%s'", uniqueSegmentUID, path) } originalPath = paths.Concatinate(originalPath, segment.UID) 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(err, "the delete query failed") } return nil } func (s *SpacePathStore) mapToInternalSpacePathSegment(p *types.SpacePathSegment) *spacePathSegment { res := &spacePathSegment{ ID: p.ID, UID: p.UID, UIDUnique: s.spacePathTransformation(p.UID, 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 }