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 &notEqualCondition{
   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 &notNullCondition{
   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 &notRegexCondition{
   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  }