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 }