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

     1  package repo
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    10  
    11  	"github.com/jmoiron/sqlx"
    12  
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  const (
    17  	// ForUpdateLock Represents FOR UPDATE lock clause in SELECT queries.
    18  	ForUpdateLock string = "FOR UPDATE"
    19  	// NoLock Represents missing lock clause in SELECT queries.
    20  	NoLock string = ""
    21  )
    22  
    23  // QueryBuilder is an interface for building queries about tenant scoped entities with either externally managed tenant accesses (m2m table or view) or embedded tenant in them.
    24  type QueryBuilder interface {
    25  	BuildQuery(resourceType resource.Type, tenantID string, isRebindingNeeded bool, conditions ...Condition) (string, []interface{}, error)
    26  }
    27  
    28  // QueryBuilderGlobal is an interface for building queries about global entities.
    29  type QueryBuilderGlobal interface {
    30  	BuildQueryGlobal(isRebindingNeeded bool, conditions ...Condition) (string, []interface{}, error)
    31  }
    32  
    33  type universalQueryBuilder struct {
    34  	tableName       string
    35  	selectedColumns string
    36  	tenantColumn    *string
    37  	resourceType    resource.Type
    38  }
    39  
    40  // NewQueryBuilderWithEmbeddedTenant is a constructor for QueryBuilder about entities with tenant embedded in them.
    41  func NewQueryBuilderWithEmbeddedTenant(tableName string, tenantColumn string, selectedColumns []string) QueryBuilder {
    42  	return &universalQueryBuilder{
    43  		tableName:       tableName,
    44  		selectedColumns: strings.Join(selectedColumns, ", "),
    45  		tenantColumn:    &tenantColumn,
    46  	}
    47  }
    48  
    49  // NewQueryBuilder is a constructor for QueryBuilder about entities with externally managed tenant accesses (m2m table or view)
    50  func NewQueryBuilder(tableName string, selectedColumns []string) QueryBuilder {
    51  	return &universalQueryBuilder{
    52  		tableName:       tableName,
    53  		selectedColumns: strings.Join(selectedColumns, ", "),
    54  	}
    55  }
    56  
    57  // NewQueryBuilderGlobal is a constructor for QueryBuilderGlobal about global entities.
    58  func NewQueryBuilderGlobal(resourceType resource.Type, tableName string, selectedColumns []string) QueryBuilderGlobal {
    59  	return &universalQueryBuilder{
    60  		resourceType:    resourceType,
    61  		tableName:       tableName,
    62  		selectedColumns: strings.Join(selectedColumns, ", "),
    63  	}
    64  }
    65  
    66  // BuildQueryGlobal builds a SQL query for global entities without tenant isolation.
    67  func (b *universalQueryBuilder) BuildQueryGlobal(isRebindingNeeded bool, conditions ...Condition) (string, []interface{}, error) {
    68  	return buildSelectQuery(b.tableName, b.selectedColumns, conditions, OrderByParams{}, NoLock, isRebindingNeeded)
    69  }
    70  
    71  // BuildQuery builds a SQL query for tenant scoped entities with tenant isolation subquery.
    72  // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn.
    73  // If the tenantColumn is not configured an entity with externally managed tenant accesses in m2m table / view is assumed.
    74  func (b *universalQueryBuilder) BuildQuery(resourceType resource.Type, tenantID string, isRebindingNeeded bool, conditions ...Condition) (string, []interface{}, error) {
    75  	if tenantID == "" {
    76  		return "", nil, apperrors.NewTenantRequiredError()
    77  	}
    78  
    79  	if b.tenantColumn != nil {
    80  		conditions = append(Conditions{NewEqualCondition(*b.tenantColumn, tenantID)}, conditions...)
    81  		return buildSelectQuery(b.tableName, b.selectedColumns, conditions, OrderByParams{}, NoLock, isRebindingNeeded)
    82  	}
    83  
    84  	tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenantID, false)
    85  	if err != nil {
    86  		return "", nil, err
    87  	}
    88  
    89  	conditions = append(conditions, tenantIsolation)
    90  
    91  	return buildSelectQuery(b.tableName, b.selectedColumns, conditions, OrderByParams{}, NoLock, isRebindingNeeded)
    92  }
    93  
    94  func buildSelectQueryFromTree(tableName string, selectedColumns string, conditions *ConditionTree, orderByParams OrderByParams, lockClause string, isRebindingNeeded bool) (string, []interface{}, error) {
    95  	var stmtBuilder strings.Builder
    96  
    97  	stmtBuilder.WriteString(fmt.Sprintf("SELECT %s FROM %s", selectedColumns, tableName))
    98  	var allArgs []interface{}
    99  	if conditions != nil {
   100  		stmtBuilder.WriteString(" WHERE ")
   101  		var subquery string
   102  		subquery, allArgs = conditions.BuildSubquery()
   103  		stmtBuilder.WriteString(subquery)
   104  	}
   105  
   106  	err := writeOrderByPart(&stmtBuilder, orderByParams)
   107  	if err != nil {
   108  		return "", nil, errors.Wrap(err, "while writing order by part")
   109  	}
   110  
   111  	writeLockClause(&stmtBuilder, lockClause)
   112  
   113  	if isRebindingNeeded {
   114  		return getQueryFromBuilder(stmtBuilder), allArgs, nil
   115  	}
   116  	return stmtBuilder.String(), allArgs, nil
   117  }
   118  
   119  // TODO: Refactor builder
   120  func buildSelectQuery(tableName string, selectedColumns string, conditions Conditions, orderByParams OrderByParams, lockClause string, isRebindingNeeded bool) (string, []interface{}, error) {
   121  	var stmtBuilder strings.Builder
   122  
   123  	stmtBuilder.WriteString(fmt.Sprintf("SELECT %s FROM %s", selectedColumns, tableName))
   124  	if len(conditions) > 0 {
   125  		stmtBuilder.WriteString(" WHERE")
   126  	}
   127  
   128  	err := writeEnumeratedConditions(&stmtBuilder, conditions)
   129  	if err != nil {
   130  		return "", nil, errors.Wrap(err, "while writing enumerated conditions.")
   131  	}
   132  
   133  	allArgs := getAllArgs(conditions)
   134  
   135  	err = writeOrderByPart(&stmtBuilder, orderByParams)
   136  	if err != nil {
   137  		return "", nil, errors.Wrap(err, "while writing order by part")
   138  	}
   139  
   140  	writeLockClause(&stmtBuilder, lockClause)
   141  
   142  	if isRebindingNeeded {
   143  		return getQueryFromBuilder(stmtBuilder), allArgs, nil
   144  	}
   145  	return stmtBuilder.String(), allArgs, nil
   146  }
   147  
   148  func buildUnionQuery(queries []string) string {
   149  	if len(queries) == 0 {
   150  		return ""
   151  	}
   152  
   153  	for i := range queries {
   154  		queries[i] = "(" + queries[i] + ")"
   155  	}
   156  
   157  	unionQuery := strings.Join(queries, " UNION ")
   158  	var stmtBuilder strings.Builder
   159  	stmtBuilder.WriteString(unionQuery)
   160  
   161  	return getQueryFromBuilder(stmtBuilder)
   162  }
   163  
   164  func buildCountQuery(tableName string, idColumn string, conditions Conditions, groupByParams GroupByParams, orderByParams OrderByParams, isRebindingNeeded bool) (string, []interface{}, error) {
   165  	isGroupByParam := false
   166  	for _, s := range groupByParams {
   167  		if idColumn == s {
   168  			isGroupByParam = true
   169  		}
   170  	}
   171  	if !isGroupByParam {
   172  		return "", nil, errors.New("id column is not in group by params")
   173  	}
   174  
   175  	var stmtBuilder strings.Builder
   176  	stmtBuilder.WriteString(fmt.Sprintf("SELECT %s AS id, COUNT(*) AS total_count FROM %s", idColumn, tableName))
   177  	if len(conditions) > 0 {
   178  		stmtBuilder.WriteString(" WHERE")
   179  	}
   180  
   181  	err := writeEnumeratedConditions(&stmtBuilder, conditions)
   182  	if err != nil {
   183  		return "", nil, errors.Wrap(err, "while writing enumerated conditions.")
   184  	}
   185  
   186  	err = writeGroupByPart(&stmtBuilder, groupByParams)
   187  	if err != nil {
   188  		return "", nil, errors.Wrap(err, "while writing order by part")
   189  	}
   190  
   191  	err = writeOrderByPart(&stmtBuilder, orderByParams)
   192  	if err != nil {
   193  		return "", nil, errors.Wrap(err, "while writing order by part")
   194  	}
   195  
   196  	allArgs := getAllArgs(conditions)
   197  
   198  	if isRebindingNeeded {
   199  		return getQueryFromBuilder(stmtBuilder), allArgs, nil
   200  	}
   201  	return stmtBuilder.String(), allArgs, nil
   202  }
   203  
   204  func buildSelectQueryWithLimitAndOffset(tableName string, selectedColumns string, conditions Conditions, orderByParams OrderByParams, limit, offset int, isRebindingNeeded bool) (string, []interface{}, error) {
   205  	query, args, err := buildSelectQuery(tableName, selectedColumns, conditions, orderByParams, NoLock, isRebindingNeeded)
   206  	if err != nil {
   207  		return "", nil, err
   208  	}
   209  
   210  	var stmtBuilder strings.Builder
   211  	stmtBuilder.WriteString(query)
   212  
   213  	err = writeLimitPart(&stmtBuilder)
   214  	if err != nil {
   215  		return "", nil, err
   216  	}
   217  	args = append(args, limit)
   218  
   219  	err = writeOffsetPart(&stmtBuilder)
   220  	if err != nil {
   221  		return "", nil, err
   222  	}
   223  	args = append(args, offset)
   224  
   225  	if isRebindingNeeded {
   226  		return getQueryFromBuilder(stmtBuilder), args, nil
   227  	}
   228  	return stmtBuilder.String(), args, nil
   229  }
   230  
   231  func getAllArgs(conditions Conditions) []interface{} {
   232  	var allArgs []interface{}
   233  
   234  	for _, cond := range conditions {
   235  		if argVal, ok := cond.GetQueryArgs(); ok {
   236  			allArgs = append(allArgs, argVal...)
   237  		}
   238  	}
   239  	return allArgs
   240  }
   241  
   242  func writeEnumeratedConditions(builder *strings.Builder, conditions Conditions) error {
   243  	if builder == nil {
   244  		return apperrors.NewInternalError("builder cannot be nil")
   245  	}
   246  
   247  	conditionsToJoin := make([]string, 0, len(conditions))
   248  	for _, cond := range conditions {
   249  		conditionsToJoin = append(conditionsToJoin, cond.GetQueryPart())
   250  	}
   251  
   252  	builder.WriteString(" ")
   253  	builder.WriteString(strings.Join(conditionsToJoin, " AND "))
   254  
   255  	return nil
   256  }
   257  
   258  const anyKeyExistsOp = "?|"
   259  const anyKeyExistsOpPlaceholder = "{{anyKeyExistsOp}}"
   260  
   261  const allKeysExistOp = "?&"
   262  const allKeysExistOpPlaceholder = "{{allKeysExistOp}}"
   263  
   264  const singleKeyExistsOp = "] ? "
   265  const singleKeyExistsOpPlaceholder = "{{singleKeyExistsOp}}"
   266  
   267  var tempReplace = []string{
   268  	anyKeyExistsOp, anyKeyExistsOpPlaceholder,
   269  	allKeysExistOp, allKeysExistOpPlaceholder,
   270  	singleKeyExistsOp, singleKeyExistsOpPlaceholder,
   271  }
   272  
   273  var reverseTempReplace = []string{
   274  	anyKeyExistsOpPlaceholder, anyKeyExistsOp,
   275  	allKeysExistOpPlaceholder, allKeysExistOp,
   276  	singleKeyExistsOpPlaceholder, singleKeyExistsOp,
   277  }
   278  
   279  // sqlx doesn't detect ?| and ?& operators properly
   280  func getQueryFromBuilder(builder strings.Builder) string {
   281  	strToRebind := strings.NewReplacer(tempReplace...).Replace(builder.String())
   282  	strAfterRebind := sqlx.Rebind(sqlx.DOLLAR, strToRebind)
   283  
   284  	query := strings.NewReplacer(reverseTempReplace...).Replace(strAfterRebind)
   285  
   286  	return query
   287  }
   288  
   289  func writeOrderByPart(builder *strings.Builder, orderByParams OrderByParams) error {
   290  	if builder == nil {
   291  		return apperrors.NewInternalError("builder cannot be nil")
   292  	}
   293  
   294  	if len(orderByParams) == 0 {
   295  		return nil
   296  	}
   297  
   298  	builder.WriteString(" ORDER BY")
   299  	for idx, orderBy := range orderByParams {
   300  		if idx > 0 {
   301  			builder.WriteString(",")
   302  		}
   303  		builder.WriteString(fmt.Sprintf(" %s %s", orderBy.Field, orderBy.Dir))
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  // GroupByParams missing godoc
   310  type GroupByParams []string
   311  
   312  func writeGroupByPart(builder *strings.Builder, groupByParams GroupByParams) error {
   313  	if builder == nil {
   314  		return apperrors.NewInternalError("builder cannot be nil")
   315  	}
   316  
   317  	if len(groupByParams) == 0 {
   318  		return nil
   319  	}
   320  
   321  	builder.WriteString(" GROUP BY ")
   322  	builder.WriteString(strings.Join(groupByParams, " ,"))
   323  
   324  	return nil
   325  }
   326  
   327  func writeLimitPart(builder *strings.Builder) error {
   328  	if builder == nil {
   329  		return apperrors.NewInternalError("builder cannot be nil")
   330  	}
   331  
   332  	builder.WriteString(" LIMIT ?")
   333  	return nil
   334  }
   335  
   336  func writeOffsetPart(builder *strings.Builder) error {
   337  	if builder == nil {
   338  		return apperrors.NewInternalError("builder cannot be nil")
   339  	}
   340  
   341  	builder.WriteString(" OFFSET ?")
   342  	return nil
   343  }
   344  
   345  func writeLockClause(builder *strings.Builder, lockClause string) {
   346  	lock := strings.TrimSpace(lockClause)
   347  	if lock != NoLock {
   348  		builder.WriteString(" " + lock)
   349  	}
   350  }