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 }