github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/repo/conditions.go (about) 1 package repo 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/kyma-incubator/compass/components/director/pkg/resource" 8 "github.com/pkg/errors" 9 ) 10 11 // Condition represents an SQL condition 12 type Condition interface { 13 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 14 GetQueryPart() string 15 16 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 17 // 18 // For conditions like IN and IS NOT NULL there are no arguments to be included in the query. 19 // For conditions like = there is a placeholder which value will be returned calling this func. 20 GetQueryArgs() ([]interface{}, bool) 21 } 22 23 // Conditions is a slice of conditions 24 type Conditions []Condition 25 26 // Operator represents an SQL operator 27 type Operator string 28 29 const ( 30 31 // OR represents an OR operator 32 OR Operator = "OR" 33 34 // AND represents an AND operator 35 AND Operator = "AND" 36 ) 37 38 // ConditionTree represents a tree of conditions. The tree is constructed bottom to top. Leafs are the actual conditions. Intermediate nodes are operators joining them. 39 type ConditionTree struct { 40 // Operator represents an SQL operator used to join the children conditions. This is populated only for non-leaf nodes. 41 Operator Operator 42 // Operand is the actual condition. This is populated only for leaf nodes. 43 Operand Condition 44 // Children is a slice of conditions. This is populated only for non-leaf nodes. 45 Children []*ConditionTree 46 } 47 48 // IsLeaf returns true if the node is a leaf node 49 func (t *ConditionTree) IsLeaf() bool { 50 return len(t.Children) == 0 51 } 52 53 // BuildSubquery builds SQL subquery for a given condition tree 54 func (t *ConditionTree) BuildSubquery() (string, []interface{}) { 55 if t.IsLeaf() { 56 args, ok := t.Operand.GetQueryArgs() 57 if !ok { 58 args = []interface{}{} 59 } 60 return t.Operand.GetQueryPart(), args 61 } 62 63 args := make([]interface{}, 0) 64 queryParts := make([]string, 0, len(t.Children)) 65 for _, child := range t.Children { 66 queryPart, childArgs := child.BuildSubquery() 67 queryParts = append(queryParts, queryPart) 68 args = append(args, childArgs...) 69 } 70 71 sql := fmt.Sprintf("(%s)", strings.Join(queryParts, fmt.Sprintf(" %s ", t.Operator))) 72 return sql, args 73 } 74 75 // ConditionTreesFromConditions builds a tree of conditions from a slice of conditions. The tree is constructed bottom to top. 76 func ConditionTreesFromConditions(conditions Conditions) []*ConditionTree { 77 if len(conditions) == 0 { 78 return nil 79 } 80 children := make([]*ConditionTree, 0, len(conditions)) 81 for i := range conditions { 82 children = append(children, &ConditionTree{Operand: conditions[i]}) 83 } 84 return children 85 } 86 87 // And joins given conditions with AND operator 88 func And(children ...*ConditionTree) *ConditionTree { 89 if len(children) == 0 { 90 return nil 91 } 92 if len(children) == 1 { 93 return children[0] 94 } 95 return &ConditionTree{ 96 Operator: AND, 97 Children: children, 98 } 99 } 100 101 // Or joins given conditions with OR operator 102 func Or(children ...*ConditionTree) *ConditionTree { 103 if len(children) == 0 { 104 return nil 105 } 106 if len(children) == 1 { 107 return children[0] 108 } 109 return &ConditionTree{ 110 Operator: OR, 111 Children: children, 112 } 113 } 114 115 // NewEqualCondition represents equal SQL condition (field = val) 116 func NewEqualCondition(field string, val interface{}) Condition { 117 return &equalCondition{ 118 field: field, 119 val: val, 120 } 121 } 122 123 type equalCondition struct { 124 field string 125 val interface{} 126 } 127 128 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 129 func (c *equalCondition) GetQueryPart() string { 130 return fmt.Sprintf("%s = ?", c.field) 131 } 132 133 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 134 func (c *equalCondition) GetQueryArgs() ([]interface{}, bool) { 135 return []interface{}{c.val}, true 136 } 137 138 // NewLessThanCondition represents less than SQL condition (field < val) 139 func NewLessThanCondition(field string, val interface{}) Condition { 140 return &lessThanCondition{ 141 field: field, 142 val: val, 143 } 144 } 145 146 type lessThanCondition struct { 147 field string 148 val interface{} 149 } 150 151 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 152 func (c *lessThanCondition) GetQueryPart() string { 153 return fmt.Sprintf("%s < ?", c.field) 154 } 155 156 // GetQueryArgs returns a list of query arguments and boolean flag showing if there are placeholders for the arguments 157 func (c *lessThanCondition) GetQueryArgs() ([]interface{}, bool) { 158 return []interface{}{c.val}, true 159 } 160 161 // NewNotEqualCondition represents not equal SQL condition (field != val) 162 func NewNotEqualCondition(field string, val interface{}) Condition { 163 return ¬EqualCondition{ 164 field: field, 165 val: val, 166 } 167 } 168 169 type notEqualCondition struct { 170 field string 171 val interface{} 172 } 173 174 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 175 func (c *notEqualCondition) GetQueryPart() string { 176 return fmt.Sprintf("%s != ?", c.field) 177 } 178 179 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 180 func (c *notEqualCondition) GetQueryArgs() ([]interface{}, bool) { 181 return []interface{}{c.val}, true 182 } 183 184 // NewNotNullCondition represents SQL not null condition (field IS NOT NULL) 185 func NewNotNullCondition(field string) Condition { 186 return ¬NullCondition{ 187 field: field, 188 } 189 } 190 191 type notNullCondition struct { 192 field string 193 } 194 195 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 196 func (c *notNullCondition) GetQueryPart() string { 197 return fmt.Sprintf("%s IS NOT NULL", c.field) 198 } 199 200 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 201 func (c *notNullCondition) GetQueryArgs() ([]interface{}, bool) { 202 return nil, false 203 } 204 205 // NewNullCondition represents SQL null condition (field IS NULL) 206 func NewNullCondition(field string) Condition { 207 return &nullCondition{ 208 field: field, 209 } 210 } 211 212 type nullCondition struct { 213 field string 214 } 215 216 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 217 func (c *nullCondition) GetQueryPart() string { 218 return fmt.Sprintf("%s IS NULL", c.field) 219 } 220 221 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 222 func (c *nullCondition) GetQueryArgs() ([]interface{}, bool) { 223 return nil, false 224 } 225 226 // NewLikeCondition represents SQL like condition (field like val) 227 func NewLikeCondition(field string, val interface{}) Condition { 228 return &likeCondition{ 229 field: field, 230 val: val, 231 } 232 } 233 234 type likeCondition struct { 235 field string 236 val interface{} 237 } 238 239 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 240 func (c *likeCondition) GetQueryPart() string { 241 return fmt.Sprintf("%s ILIKE ?", c.field) 242 } 243 244 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 245 func (c *likeCondition) GetQueryArgs() ([]interface{}, bool) { 246 return []interface{}{c.val}, true 247 } 248 249 // NewInConditionForSubQuery represents SQL IN subquery (field IN (SELECT ...)) 250 func NewInConditionForSubQuery(field, subQuery string, args []interface{}) Condition { 251 return &inCondition{ 252 field: field, 253 parenthesis: subQuery, 254 args: args, 255 } 256 } 257 258 type inCondition struct { 259 field string 260 parenthesis string 261 args []interface{} 262 } 263 264 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 265 func (c *inCondition) GetQueryPart() string { 266 return fmt.Sprintf("%s IN (%s)", c.field, c.parenthesis) 267 } 268 269 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 270 func (c *inCondition) GetQueryArgs() ([]interface{}, bool) { 271 return c.args, true 272 } 273 274 // NewInConditionForStringValues represents SQL IN condition (field IN (?, ?, ...)) 275 func NewInConditionForStringValues(field string, values []string) Condition { 276 parenthesisParams := make([]string, 0, len(values)) 277 args := make([]interface{}, 0, len(values)) 278 for _, value := range values { 279 parenthesisParams = append(parenthesisParams, "?") 280 args = append(args, value) 281 } 282 283 return &inCondition{ 284 field: field, 285 args: args, 286 parenthesis: strings.Join(parenthesisParams, ", "), 287 } 288 } 289 290 type notRegexCondition struct { 291 field string 292 value string 293 } 294 295 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 296 func (c *notRegexCondition) GetQueryPart() string { 297 return fmt.Sprintf("NOT %s ~ ?", c.field) 298 } 299 300 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 301 func (c *notRegexCondition) GetQueryArgs() ([]interface{}, bool) { 302 return []interface{}{c.value}, true 303 } 304 305 // NewNotRegexConditionString represents SQL regex not match condition 306 func NewNotRegexConditionString(field string, value string) Condition { 307 return ¬RegexCondition{ 308 field: field, 309 value: value, 310 } 311 } 312 313 // NewJSONCondition represents PostgreSQL JSONB contains condition 314 func NewJSONCondition(field string, val interface{}) Condition { 315 return &jsonCondition{ 316 field: field, 317 val: val, 318 } 319 } 320 321 type jsonCondition struct { 322 field string 323 val interface{} 324 } 325 326 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 327 func (c *jsonCondition) GetQueryPart() string { 328 return fmt.Sprintf("%s @> ?", c.field) 329 } 330 331 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 332 func (c *jsonCondition) GetQueryArgs() ([]interface{}, bool) { 333 return []interface{}{c.val}, true 334 } 335 336 type jsonArrAnyMatchCondition struct { 337 field string 338 val []interface{} 339 } 340 341 // NewJSONArrAnyMatchCondition represents PostgreSQL JSONB array any element match condition 342 func NewJSONArrAnyMatchCondition(field string, val []interface{}) Condition { 343 return &jsonArrAnyMatchCondition{ 344 field: field, 345 val: val, 346 } 347 } 348 349 // NewJSONArrMatchAnyStringCondition represents PostgreSQL JSONB string array any element match condition 350 func NewJSONArrMatchAnyStringCondition(field string, values ...string) Condition { 351 valuesInterfaceSlice := make([]interface{}, 0, len(values)) 352 for _, v := range values { 353 valuesInterfaceSlice = append(valuesInterfaceSlice, v) 354 } 355 356 return NewJSONArrAnyMatchCondition(field, valuesInterfaceSlice) 357 } 358 359 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 360 func (c *jsonArrAnyMatchCondition) GetQueryPart() string { 361 valHolders := make([]string, 0, len(c.val)) 362 for range c.val { 363 valHolders = append(valHolders, "?") 364 } 365 366 return fmt.Sprintf("%s ?| array[%s]", c.field, strings.Join(valHolders, ",")) 367 } 368 369 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 370 func (c *jsonArrAnyMatchCondition) GetQueryArgs() ([]interface{}, bool) { 371 return c.val, true 372 } 373 374 type tenantIsolationCondition struct { 375 sql string 376 args []interface{} 377 } 378 379 // GetQueryPart returns formatted string that will be included in the SQL query for a given condition 380 func (c *tenantIsolationCondition) GetQueryPart() string { 381 return c.sql 382 } 383 384 // GetQueryArgs returns a boolean flag if the condition contain arguments and the actual arguments 385 func (c *tenantIsolationCondition) GetQueryArgs() ([]interface{}, bool) { 386 return c.args, true 387 } 388 389 // NewTenantIsolationCondition is a tenant isolation SQL subquery for entities that have tenant accesses managed outside of 390 // the entity table (m2m table or view). Conditionally an owner check is added to the subquery. 391 // In case of resource.BundleInstanceAuth additional embedded owner check is added. 392 func NewTenantIsolationCondition(resourceType resource.Type, tenant string, ownerCheck bool) (Condition, error) { 393 return newTenantIsolationConditionWithPlaceholder(resourceType, tenant, ownerCheck, true) 394 } 395 396 // NewTenantIsolationConditionForNamedArgs is the same as NewTenantIsolationCondition, but for update queries which use named args. 397 func NewTenantIsolationConditionForNamedArgs(resourceType resource.Type, tenant string, ownerCheck bool) (Condition, error) { 398 return newTenantIsolationConditionWithPlaceholder(resourceType, tenant, ownerCheck, false) 399 } 400 401 func newTenantIsolationConditionWithPlaceholder(resourceType resource.Type, tenant string, ownerCheck bool, positionalArgs bool) (Condition, error) { 402 m2mTable, ok := resourceType.TenantAccessTable() 403 if !ok { 404 return nil, errors.Errorf("entity %s does not have access table", resourceType) 405 } 406 407 var stmtBuilder strings.Builder 408 var args []interface{} 409 410 if positionalArgs { 411 stmtBuilder.WriteString(fmt.Sprintf("(id IN (SELECT %s FROM %s WHERE %s = ?", M2MResourceIDColumn, m2mTable, M2MTenantIDColumn)) 412 args = append(args, tenant) 413 } else { 414 stmtBuilder.WriteString(fmt.Sprintf("(id IN (SELECT %s FROM %s WHERE %s = :tenant_id", M2MResourceIDColumn, m2mTable, M2MTenantIDColumn)) 415 } 416 417 if ownerCheck { 418 stmtBuilder.WriteString(fmt.Sprintf(" AND %s = true", M2MOwnerColumn)) 419 } 420 stmtBuilder.WriteString(")") 421 422 if resourceType == resource.BundleInstanceAuth { 423 if positionalArgs { 424 stmtBuilder.WriteString(" OR owner_id = ?") 425 args = append(args, tenant) 426 } else { 427 stmtBuilder.WriteString(" OR owner_id = :owner_id") 428 } 429 } 430 stmtBuilder.WriteString(")") 431 432 return &tenantIsolationCondition{sql: stmtBuilder.String(), args: args}, nil 433 }