drone/app/store/database/label.go

465 lines
12 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"
"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/harness/gitness/types/enum"
"github.com/Masterminds/squirrel"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
const (
labelColumns = `
label_space_id
,label_repo_id
,label_scope
,label_key
,label_description
,label_type
,label_color
,label_created
,label_updated
,label_created_by
,label_updated_by`
labelSelectBase = `SELECT label_id, ` + labelColumns + ` FROM labels`
)
type label struct {
ID int64 `db:"label_id"`
SpaceID null.Int `db:"label_space_id"`
RepoID null.Int `db:"label_repo_id"`
Scope int64 `db:"label_scope"`
Key string `db:"label_key"`
Description string `db:"label_description"`
Type enum.LabelType `db:"label_type"`
Color enum.LabelColor `db:"label_color"`
ValueCount int64 `db:"label_value_count"`
Created int64 `db:"label_created"`
Updated int64 `db:"label_updated"`
CreatedBy int64 `db:"label_created_by"`
UpdatedBy int64 `db:"label_updated_by"`
}
type labelInfo struct {
LabelID int64 `db:"label_id"`
SpaceID null.Int `db:"label_space_id"`
RepoID null.Int `db:"label_repo_id"`
Scope int64 `db:"label_scope"`
Key string `db:"label_key"`
Type enum.LabelType `db:"label_type"`
LabelColor enum.LabelColor `db:"label_color"`
}
type labelStore struct {
db *sqlx.DB
}
func NewLabelStore(
db *sqlx.DB,
) store.LabelStore {
return &labelStore{
db: db,
}
}
var _ store.LabelStore = (*labelStore)(nil)
func (s *labelStore) Define(ctx context.Context, lbl *types.Label) error {
const sqlQuery = `
INSERT INTO labels (` + labelColumns + `)` + `
values (
:label_space_id
,:label_repo_id
,:label_scope
,:label_key
,:label_description
,:label_type
,:label_color
,:label_created
,:label_updated
,:label_created_by
,:label_updated_by
)
RETURNING label_id`
db := dbtx.GetAccessor(ctx, s.db)
query, args, err := db.BindNamed(sqlQuery, mapInternalLabel(lbl))
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind query")
}
if err = db.QueryRowContext(ctx, query, args...).Scan(&lbl.ID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to create label")
}
return nil
}
func (s *labelStore) Update(ctx context.Context, lbl *types.Label) error {
const sqlQuery = `
UPDATE labels SET
label_key = :label_key
,label_description = :label_description
,label_type = :label_type
,label_color = :label_color
,label_updated = :label_updated
,label_updated_by = :label_updated_by
WHERE label_id = :label_id`
db := dbtx.GetAccessor(ctx, s.db)
query, args, err := db.BindNamed(sqlQuery, mapInternalLabel(lbl))
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind query")
}
if _, err := db.ExecContext(ctx, query, args...); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to update label")
}
return nil
}
func (s *labelStore) IncrementValueCount(
ctx context.Context,
labelID int64,
increment int,
) (int64, error) {
const sqlQuery = `
UPDATE labels
SET label_value_count = label_value_count + $1
WHERE label_id = $2
RETURNING label_value_count
`
db := dbtx.GetAccessor(ctx, s.db)
var valueCount int64
if err := db.QueryRowContext(ctx, sqlQuery, increment, labelID).Scan(&valueCount); err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed to increment label_value_count")
}
return valueCount, nil
}
func (s *labelStore) Find(
ctx context.Context,
spaceID, repoID *int64,
key string,
) (*types.Label, error) {
const sqlQuery = labelSelectBase + `
WHERE (label_space_id = $1 OR label_repo_id = $2) AND LOWER(label_key) = LOWER($3)`
db := dbtx.GetAccessor(ctx, s.db)
var dst label
if err := db.GetContext(ctx, &dst, sqlQuery, spaceID, repoID, key); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label")
}
return mapLabel(&dst), nil
}
func (s *labelStore) FindByID(ctx context.Context, id int64) (*types.Label, error) {
const sqlQuery = labelSelectBase + `
WHERE label_id = $1`
db := dbtx.GetAccessor(ctx, s.db)
var dst label
if err := db.GetContext(ctx, &dst, sqlQuery, id); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label")
}
return mapLabel(&dst), nil
}
func (s *labelStore) Delete(ctx context.Context, spaceID, repoID *int64, name string) error {
const sqlQuery = `
DELETE FROM labels
WHERE (label_space_id = $1 OR label_repo_id = $2) AND LOWER(label_key) = LOWER($3)`
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, sqlQuery, spaceID, repoID, name); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to delete label")
}
return nil
}
// List returns a list of pull requests for a repo or space.
func (s *labelStore) List(
ctx context.Context,
spaceID, repoID *int64,
filter *types.LabelFilter,
) ([]*types.Label, error) {
stmt := database.Builder.
Select(`label_id, ` + labelColumns + `, label_value_count`).
From("labels").
OrderBy("label_key")
stmt = stmt.Where("(label_space_id = ? OR label_repo_id = ?)", spaceID, repoID)
stmt = stmt.Limit(database.Limit(filter.Size))
stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))
if filter.Query != "" {
stmt = stmt.Where(
"LOWER(label_key) LIKE '%' || LOWER(?) || '%'", filter.Query)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
var dst []*label
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels")
}
return mapSliceLabel(dst), nil
}
func (s *labelStore) ListInScopes(
ctx context.Context,
repoID int64,
spaceIDs []int64,
filter *types.LabelFilter,
) ([]*types.Label, error) {
stmt := database.Builder.
Select(`label_id, ` + labelColumns + `, label_value_count`).
From("labels")
stmt = stmt.Where(squirrel.Or{
squirrel.Eq{"label_space_id": spaceIDs},
squirrel.Eq{"label_repo_id": repoID},
}).
OrderBy("label_key").
OrderBy("label_scope")
stmt = stmt.Limit(database.Limit(filter.Size))
stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))
if filter.Query != "" {
stmt = stmt.Where(
"LOWER(label_key) LIKE '%' || LOWER(?) || '%'", filter.Query)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
var dst []*label
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels in hierarchy")
}
return mapSliceLabel(dst), nil
}
func (s *labelStore) ListInfosInScopes(
ctx context.Context,
repoID int64,
spaceIDs []int64,
filter *types.AssignableLabelFilter,
) ([]*types.LabelInfo, error) {
stmt := database.Builder.
Select(`
label_id
,label_space_id
,label_repo_id
,label_scope
,label_key
,label_type
,label_color`).
From("labels").
Where(squirrel.Or{
squirrel.Eq{"label_space_id": spaceIDs},
squirrel.Eq{"label_repo_id": repoID},
}).
OrderBy("label_key").
OrderBy("label_scope")
stmt = stmt.Limit(database.Limit(filter.Size))
stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))
if filter.Query != "" {
stmt = stmt.Where(
"LOWER(label_key) LIKE '%' || LOWER(?) || '%'", filter.Query)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
var dst []*labelInfo
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels")
}
return mapLabelInfos(dst), nil
}
func (s *labelStore) CountInSpace(
ctx context.Context,
spaceID int64,
filter *types.LabelFilter,
) (int64, error) {
const sqlQuery = `SELECT COUNT(*) FROM labels WHERE label_space_id = $1`
return s.count(ctx, sqlQuery, spaceID, filter)
}
func (s *labelStore) CountInRepo(
ctx context.Context,
repoID int64,
filter *types.LabelFilter,
) (int64, error) {
const sqlQuery = `SELECT COUNT(*) FROM labels WHERE label_repo_id = $1`
return s.count(ctx, sqlQuery, repoID, filter)
}
func (s labelStore) count(
ctx context.Context,
sqlQuery string,
scopeID int64,
filter *types.LabelFilter,
) (int64, error) {
sqlQuery += `
AND LOWER(label_key) LIKE '%' || LOWER($2) || '%'`
db := dbtx.GetAccessor(ctx, s.db)
var count int64
if err := db.QueryRowContext(ctx, sqlQuery, scopeID, filter.Query).Scan(&count); err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed to count labels")
}
return count, nil
}
func (s *labelStore) CountInScopes(
ctx context.Context,
repoID int64,
spaceIDs []int64,
filter *types.LabelFilter,
) (int64, error) {
stmt := database.Builder.Select("COUNT(*)").
From("labels").
Where(squirrel.Or{
squirrel.Eq{"label_space_id": spaceIDs},
squirrel.Eq{"label_repo_id": repoID},
}).
Where("LOWER(label_key) LIKE '%' || LOWER(?) || '%'", filter.Query)
sql, args, err := stmt.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
var count int64
if err = db.QueryRowContext(ctx, sql, args...).Scan(&count); err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed to count labels in scopes")
}
return count, nil
}
func mapLabel(lbl *label) *types.Label {
return &types.Label{
ID: lbl.ID,
SpaceID: lbl.SpaceID.Ptr(),
RepoID: lbl.RepoID.Ptr(),
Scope: lbl.Scope,
Key: lbl.Key,
Type: lbl.Type,
Description: lbl.Description,
ValueCount: lbl.ValueCount,
Color: lbl.Color,
Created: lbl.Created,
Updated: lbl.Updated,
CreatedBy: lbl.CreatedBy,
UpdatedBy: lbl.UpdatedBy,
}
}
func mapSliceLabel(dbLabels []*label) []*types.Label {
result := make([]*types.Label, len(dbLabels))
for i, lbl := range dbLabels {
result[i] = mapLabel(lbl)
}
return result
}
func mapInternalLabel(lbl *types.Label) *label {
return &label{
ID: lbl.ID,
SpaceID: null.IntFromPtr(lbl.SpaceID),
RepoID: null.IntFromPtr(lbl.RepoID),
Scope: lbl.Scope,
Key: lbl.Key,
Description: lbl.Description,
Type: lbl.Type,
Color: lbl.Color,
Created: lbl.Created,
Updated: lbl.Updated,
CreatedBy: lbl.CreatedBy,
UpdatedBy: lbl.UpdatedBy,
}
}
func mapLabelInfo(internal *labelInfo) *types.LabelInfo {
return &types.LabelInfo{
ID: internal.LabelID,
RepoID: internal.RepoID.Ptr(),
SpaceID: internal.SpaceID.Ptr(),
Scope: internal.Scope,
Key: internal.Key,
Type: internal.Type,
Color: internal.LabelColor,
}
}
func mapLabelInfos(
dbLabels []*labelInfo,
) []*types.LabelInfo {
result := make([]*types.LabelInfo, len(dbLabels))
for i, lbl := range dbLabels {
result[i] = mapLabelInfo(lbl)
}
return result
}