github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/label/query.go (about)

     1  package label
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/kyma-incubator/compass/components/director/internal/repo"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/google/uuid"
    13  	"github.com/kyma-incubator/compass/components/director/internal/labelfilter"
    14  	"github.com/kyma-incubator/compass/components/director/internal/model"
    15  )
    16  
    17  // SetCombination type defines possible result set combination for querying
    18  type SetCombination string
    19  
    20  const (
    21  	// IntersectSet missing godoc
    22  	IntersectSet SetCombination = "INTERSECT"
    23  	// ExceptSet missing godoc
    24  	ExceptSet SetCombination = "EXCEPT"
    25  	// UnionSet missing godoc
    26  	UnionSet                   SetCombination = "UNION"
    27  	scenariosLabelKey          string         = "SCENARIOS"
    28  	runtimeTypeLabelKey        string         = "runtimeType"
    29  	globalSubaccountIDLabelKey string         = "global_subaccount_id"
    30  	stmtPrefixFormat           string         = `SELECT "%s" FROM %s WHERE "%s" IS NOT NULL AND`
    31  	stmtPrefixGlobalFormat     string         = `SELECT "%s" FROM %s WHERE "%s" IS NOT NULL`
    32  )
    33  
    34  type queryFilter struct {
    35  	Exists bool
    36  }
    37  
    38  // FilterQuery builds select query for given filters
    39  //
    40  // It supports querying defined by `queryFor` parameter. All queries are created
    41  // in the context of given tenant
    42  func FilterQuery(queryFor model.LabelableObject, setCombination SetCombination, tenant uuid.UUID, filter []*labelfilter.LabelFilter) (string, []interface{}, error) {
    43  	return filterQuery(queryFor, setCombination, tenant, filter, false)
    44  }
    45  
    46  // FilterSubquery builds select sub query for given filters that can be appended to other query
    47  //
    48  // It supports querying defined by `queryFor` parameter. All queries are created
    49  // in the context of given tenant
    50  func FilterSubquery(queryFor model.LabelableObject, setCombination SetCombination, tenant uuid.UUID, filter []*labelfilter.LabelFilter) (string, []interface{}, error) {
    51  	return filterQuery(queryFor, setCombination, tenant, filter, true)
    52  }
    53  
    54  // FilterQueryGlobal builds select query for given filters
    55  //
    56  // It supports querying defined by `queryFor` parameter. All queries are created
    57  // in the global context
    58  func FilterQueryGlobal(queryFor model.LabelableObject, setCombination SetCombination, filters []*labelfilter.LabelFilter) (string, []interface{}, error) {
    59  	if filters == nil {
    60  		return "", nil, nil
    61  	}
    62  
    63  	objectField := labelableObjectField(queryFor)
    64  
    65  	stmtPrefix := fmt.Sprintf(stmtPrefixGlobalFormat, objectField, tableName, objectField)
    66  
    67  	return buildFilterQuery(stmtPrefix, nil, setCombination, filters, false)
    68  }
    69  
    70  func filterQuery(queryFor model.LabelableObject, setCombination SetCombination, tenant uuid.UUID, filter []*labelfilter.LabelFilter, isSubQuery bool) (string, []interface{}, error) {
    71  	if filter == nil {
    72  		return "", nil, nil
    73  	}
    74  
    75  	objectField := labelableObjectField(queryFor)
    76  
    77  	var cond repo.Condition
    78  	if queryFor == model.TenantLabelableObject {
    79  		cond = repo.NewEqualCondition("tenant_id", tenant)
    80  	} else {
    81  		var err error
    82  		cond, err = repo.NewTenantIsolationCondition(queryFor.GetResourceType(), tenant.String(), false)
    83  		if err != nil {
    84  			return "", nil, err
    85  		}
    86  	}
    87  
    88  	stmtPrefixFormatWithTenantIsolation := stmtPrefixFormat + " " + cond.GetQueryPart()
    89  	stmtPrefix := fmt.Sprintf(stmtPrefixFormatWithTenantIsolation, objectField, tableName, objectField)
    90  	var stmtPrefixArgs []interface{}
    91  	stmtPrefixArgs = append(stmtPrefixArgs, tenant)
    92  
    93  	return buildFilterQuery(stmtPrefix, stmtPrefixArgs, setCombination, filter, isSubQuery)
    94  }
    95  
    96  func buildFilterQuery(stmtPrefix string, stmtPrefixArgs []interface{}, setCombination SetCombination, filters []*labelfilter.LabelFilter, isSubQuery bool) (string, []interface{}, error) {
    97  	var queryBuilder strings.Builder
    98  
    99  	args := make([]interface{}, 0, len(filters))
   100  	for idx, lblFilter := range filters {
   101  		if idx > 0 || isSubQuery {
   102  			queryBuilder.WriteString(fmt.Sprintf(` %s `, setCombination))
   103  		}
   104  
   105  		queryBuilder.WriteString(stmtPrefix)
   106  		if len(stmtPrefixArgs) > 0 {
   107  			args = append(args, stmtPrefixArgs...)
   108  		}
   109  
   110  		// TODO: for optimization it can be detected if the given Key was already added to the query
   111  		// if so, it can be omitted
   112  
   113  		shouldKeyExists := true
   114  		var err error
   115  		if lblFilter.Key == globalSubaccountIDLabelKey {
   116  			shouldKeyExists, err = shouldGlobalSubaccountExists(lblFilter.Query)
   117  			if err != nil {
   118  				return "", nil, errors.Wrap(err, "while determining if global_subaccount_id exists")
   119  			}
   120  		}
   121  
   122  		if shouldKeyExists {
   123  			queryBuilder.WriteString(` AND "key" = ?`)
   124  			args = append(args, lblFilter.Key)
   125  		}
   126  
   127  		if lblFilter.Query != nil {
   128  			queryValue := *lblFilter.Query
   129  			// Handling the Scenarios label case - we assume that Query is
   130  			// in SQL/JSON path format supported by PostgreSQL 12. Till it
   131  			// is not production ready, we need to transform the Query from
   132  			// SQL/JSON path to old JSON queries.
   133  			if strings.ToUpper(lblFilter.Key) == scenariosLabelKey || lblFilter.Key == runtimeTypeLabelKey {
   134  				extractedValues, err := ExtractValueFromJSONPath(queryValue)
   135  				if err != nil {
   136  					return "", nil, errors.Wrap(err, "while extracting value from JSON path")
   137  				}
   138  
   139  				args = append(args, extractedValues...)
   140  
   141  				queryValues := make([]string, len(extractedValues))
   142  				for idx := range extractedValues {
   143  					queryValues[idx] = "?"
   144  				}
   145  				queryValue = `array[` + strings.Join(queryValues, ",") + `]`
   146  
   147  				queryBuilder.WriteString(fmt.Sprintf(` AND "value" ?| %s`, queryValue))
   148  			} else if lblFilter.Key == globalSubaccountIDLabelKey && !shouldKeyExists {
   149  				queryBuilder.WriteString(` AND "app_id" NOT IN (SELECT "app_id" FROM public.labels WHERE key = 'global_subaccount_id' AND "app_id" IS NOT NULL)`)
   150  			} else {
   151  				args = append(args, queryValue)
   152  				queryBuilder.WriteString(` AND "value" @> ?`)
   153  			}
   154  		}
   155  	}
   156  
   157  	return queryBuilder.String(), args, nil
   158  }
   159  
   160  func shouldGlobalSubaccountExists(filter *string) (bool, error) {
   161  	if filter == nil {
   162  		return true, nil
   163  	}
   164  
   165  	// check if *filter is valid json
   166  	var js map[string]interface{}
   167  	if err := json.Unmarshal([]byte(*filter), &js); err != nil {
   168  		//lint:ignore nilerr can proceed
   169  		return true, nil
   170  	}
   171  
   172  	query := &queryFilter{}
   173  	if err := json.Unmarshal([]byte(*filter), query); err != nil {
   174  		return false, err
   175  	}
   176  
   177  	return query.Exists, nil
   178  }