Commit d83d9ec3df6e0a1fc87520a53fd7bc4deff87d54

Authored by Andrii Shvaika
1 parent 112b9250

Fix for sort order in search by attributes or telemetry

@@ -86,6 +86,7 @@ BEGIN @@ -86,6 +86,7 @@ BEGIN
86 DROP INDEX IF EXISTS idx_asset_customer_id; 86 DROP INDEX IF EXISTS idx_asset_customer_id;
87 DROP INDEX IF EXISTS idx_asset_customer_id_and_type; 87 DROP INDEX IF EXISTS idx_asset_customer_id_and_type;
88 DROP INDEX IF EXISTS idx_asset_type; 88 DROP INDEX IF EXISTS idx_asset_type;
  89 + DROP INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts;
89 END; 90 END;
90 $$; 91 $$;
91 92
@@ -105,6 +106,7 @@ BEGIN @@ -105,6 +106,7 @@ BEGIN
105 CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id); 106 CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
106 CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); 107 CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
107 CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); 108 CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);
  109 + CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);
108 END; 110 END;
109 $$; 111 $$;
110 112
@@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
39 import org.thingsboard.server.common.data.query.EntityDataSortOrder; 39 import org.thingsboard.server.common.data.query.EntityDataSortOrder;
40 import org.thingsboard.server.common.data.query.EntityFilter; 40 import org.thingsboard.server.common.data.query.EntityFilter;
41 import org.thingsboard.server.common.data.query.EntityFilterType; 41 import org.thingsboard.server.common.data.query.EntityFilterType;
  42 +import org.thingsboard.server.common.data.query.EntityKeyType;
42 import org.thingsboard.server.common.data.query.EntityListFilter; 43 import org.thingsboard.server.common.data.query.EntityListFilter;
43 import org.thingsboard.server.common.data.query.EntityNameFilter; 44 import org.thingsboard.server.common.data.query.EntityNameFilter;
44 import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; 45 import org.thingsboard.server.common.data.query.EntitySearchQueryFilter;
@@ -265,7 +266,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { @@ -265,7 +266,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
265 266
266 267
267 String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); 268 String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping);
268 - String latestJoins = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings); 269 + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true);
  270 + String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false);
269 String whereClause = DefaultEntityQueryRepository.this.buildWhere(ctx, latestFiltersMapping, query.getEntityFilter().getType()); 271 String whereClause = DefaultEntityQueryRepository.this.buildWhere(ctx, latestFiltersMapping, query.getEntityFilter().getType());
270 String textSearchQuery = DefaultEntityQueryRepository.this.buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); 272 String textSearchQuery = DefaultEntityQueryRepository.this.buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch());
271 String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); 273 String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType);
@@ -286,29 +288,44 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { @@ -286,29 +288,44 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
286 topSelection = topSelection + ", " + latestSelection; 288 topSelection = topSelection + ", " + latestSelection;
287 } 289 }
288 290
289 - String fromClause = String.format("from (select %s from (select %s from %s e where %s) entities %s %s) result %s", 291 + String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s %s) result %s",
  292 + "entities.*",
  293 + entityFieldsSelection,
  294 + addEntityTableQuery(ctx, query.getEntityFilter()),
  295 + entityWhereClause,
  296 + latestJoinsCnt,
  297 + whereClause,
  298 + textSearchQuery);
  299 +
  300 + String fromClauseData = String.format("from (select %s from (select %s from %s e where %s) entities %s %s) result %s",
290 topSelection, 301 topSelection,
291 entityFieldsSelection, 302 entityFieldsSelection,
292 addEntityTableQuery(ctx, query.getEntityFilter()), 303 addEntityTableQuery(ctx, query.getEntityFilter()),
293 entityWhereClause, 304 entityWhereClause,
294 - latestJoins, 305 + latestJoinsData,
295 whereClause, 306 whereClause,
296 textSearchQuery); 307 textSearchQuery);
297 308
298 - int totalElements = jdbcTemplate.queryForObject(String.format("select count(*) %s", fromClause), ctx, Integer.class); 309 + if (!StringUtils.isEmpty(pageLink.getTextSearch())) {
  310 + //Unfortunately, we need to sacrifice performance in case of full text search, because it is applied to all joined records.
  311 + fromClauseCount = fromClauseData;
  312 + }
  313 + String countQuery = String.format("select count(id) %s", fromClauseCount);
  314 + int totalElements = jdbcTemplate.queryForObject(countQuery, ctx, Integer.class);
299 315
300 - String dataQuery = String.format("select * %s", fromClause); 316 + String dataQuery = String.format("select * %s", fromClauseData);
301 317
302 EntityDataSortOrder sortOrder = pageLink.getSortOrder(); 318 EntityDataSortOrder sortOrder = pageLink.getSortOrder();
303 if (sortOrder != null) { 319 if (sortOrder != null) {
304 Optional<EntityKeyMapping> sortOrderMappingOpt = mappings.stream().filter(EntityKeyMapping::isSortOrder).findFirst(); 320 Optional<EntityKeyMapping> sortOrderMappingOpt = mappings.stream().filter(EntityKeyMapping::isSortOrder).findFirst();
305 if (sortOrderMappingOpt.isPresent()) { 321 if (sortOrderMappingOpt.isPresent()) {
306 EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get(); 322 EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get();
307 - dataQuery = String.format("%s order by %s", dataQuery, sortOrderMapping.getValueAlias());  
308 - if (sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC) {  
309 - dataQuery += " asc"; 323 + String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc";
  324 + if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) {
  325 + dataQuery = String.format("%s order by %s %s", dataQuery, sortOrderMapping.getValueAlias(), direction);
310 } else { 326 } else {
311 - dataQuery += " desc"; 327 + dataQuery = String.format("%s order by %s %s, %s %s", dataQuery,
  328 + sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction);
312 } 329 }
313 } 330 }
314 } 331 }
@@ -270,9 +270,10 @@ public class EntityKeyMapping { @@ -270,9 +270,10 @@ public class EntityKeyMapping {
270 Collectors.joining(", ")); 270 Collectors.joining(", "));
271 } 271 }
272 272
273 - public static String buildLatestJoins(QueryContext ctx, EntityFilter entityFilter, EntityType entityType, List<EntityKeyMapping> latestMappings) {  
274 - return latestMappings.stream().map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)).collect(  
275 - Collectors.joining(" ")); 273 + public static String buildLatestJoins(QueryContext ctx, EntityFilter entityFilter, EntityType entityType, List<EntityKeyMapping> latestMappings, boolean countQuery) {
  274 + return latestMappings.stream().filter(mapping -> !countQuery || mapping.hasFilter())
  275 + .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)).collect(
  276 + Collectors.joining(" "));
276 } 277 }
277 278
278 public static String buildQuery(QueryContext ctx, List<EntityKeyMapping> mappings, EntityFilterType filterType) { 279 public static String buildQuery(QueryContext ctx, List<EntityKeyMapping> mappings, EntityFilterType filterType) {
@@ -363,19 +364,14 @@ public class EntityKeyMapping { @@ -363,19 +364,14 @@ public class EntityKeyMapping {
363 } 364 }
364 365
365 private String buildAttributeSelection() { 366 private String buildAttributeSelection() {
366 - String attrValAlias = getValueAlias();  
367 - String attrTsAlias = getTsAlias();  
368 - String attrValSelection =  
369 - String.format("(coalesce(cast(%s.bool_v as varchar), '') || " +  
370 - "coalesce(%s.str_v, '') || " +  
371 - "coalesce(cast(%s.long_v as varchar), '') || " +  
372 - "coalesce(cast(%s.dbl_v as varchar), '') || " +  
373 - "coalesce(cast(%s.json_v as varchar), '')) as %s", alias, alias, alias, alias, alias, attrValAlias);  
374 - String attrTsSelection = String.format("%s.last_update_ts as %s", alias, attrTsAlias);  
375 - return String.join(", ", attrValSelection, attrTsSelection); 367 + return buildTimeSeriesOrAttrSelection(true);
376 } 368 }
377 369
378 private String buildTimeSeriesSelection() { 370 private String buildTimeSeriesSelection() {
  371 + return buildTimeSeriesOrAttrSelection(false);
  372 + }
  373 +
  374 + private String buildTimeSeriesOrAttrSelection(boolean attr) {
379 String attrValAlias = getValueAlias(); 375 String attrValAlias = getValueAlias();
380 String attrTsAlias = getTsAlias(); 376 String attrTsAlias = getTsAlias();
381 String attrValSelection = 377 String attrValSelection =
@@ -384,8 +380,25 @@ public class EntityKeyMapping { @@ -384,8 +380,25 @@ public class EntityKeyMapping {
384 "coalesce(cast(%s.long_v as varchar), '') || " + 380 "coalesce(cast(%s.long_v as varchar), '') || " +
385 "coalesce(cast(%s.dbl_v as varchar), '') || " + 381 "coalesce(cast(%s.dbl_v as varchar), '') || " +
386 "coalesce(cast(%s.json_v as varchar), '')) as %s", alias, alias, alias, alias, alias, attrValAlias); 382 "coalesce(cast(%s.json_v as varchar), '')) as %s", alias, alias, alias, alias, alias, attrValAlias);
387 - String attrTsSelection = String.format("%s.ts as %s", alias, attrTsAlias);  
388 - return String.join(", ", attrValSelection, attrTsSelection); 383 + String attrTsSelection = String.format("%s.%s as %s", alias, attr ? "last_update_ts" : "ts", attrTsAlias);
  384 + if (this.isSortOrder) {
  385 + String attrNumAlias = getSortOrderNumAlias();
  386 + String attrVarcharAlias = getSortOrderStrAlias();
  387 + String attrSortOrderSelection =
  388 + String.format("coalesce(%s.dbl_v, cast(%s.long_v as double precision), (case when %s.bool_v then 1 else 0 end)) %s," +
  389 + "coalesce(%s.str_v, cast(%s.json_v as varchar), '') %s", alias, alias, alias, attrNumAlias, alias, alias, attrVarcharAlias);
  390 + return String.join(", ", attrValSelection, attrTsSelection, attrSortOrderSelection);
  391 + } else {
  392 + return String.join(", ", attrValSelection, attrTsSelection);
  393 + }
  394 + }
  395 +
  396 + public String getSortOrderStrAlias() {
  397 + return getValueAlias() + "_so_varchar";
  398 + }
  399 +
  400 + public String getSortOrderNumAlias() {
  401 + return getValueAlias() + "_so_num";
389 } 402 }
390 403
391 private String buildKeyQuery(QueryContext ctx, String alias, KeyFilter keyFilter, 404 private String buildKeyQuery(QueryContext ctx, String alias, KeyFilter keyFilter,
@@ -425,11 +438,11 @@ public class EntityKeyMapping { @@ -425,11 +438,11 @@ public class EntityKeyMapping {
425 if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { 438 if (predicate.getType().equals(FilterPredicateType.NUMERIC)) {
426 return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); 439 return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate);
427 } else if (predicate.getType().equals(FilterPredicateType.STRING)) { 440 } else if (predicate.getType().equals(FilterPredicateType.STRING)) {
428 - if (key.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)){ 441 + if (key.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) {
429 field = ctx.getEntityType().toString(); 442 field = ctx.getEntityType().toString();
430 return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate) 443 return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate)
431 .replace("lower(" + field, "lower('" + field + "'") 444 .replace("lower(" + field, "lower('" + field + "'")
432 - .replace(field + " ","'" + field + "' "); 445 + .replace(field + " ", "'" + field + "' ");
433 } else { 446 } else {
434 return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); 447 return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate);
435 } 448 }
@@ -481,13 +494,13 @@ public class EntityKeyMapping { @@ -481,13 +494,13 @@ public class EntityKeyMapping {
481 stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '%%')", operationField, paramName, operationField, paramName); 494 stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '%%')", operationField, paramName, operationField, paramName);
482 break; 495 break;
483 case CONTAINS: 496 case CONTAINS:
484 - if (value.length()>0) { 497 + if (value.length() > 0) {
485 value = "%" + value + "%"; 498 value = "%" + value + "%";
486 } 499 }
487 stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '')", operationField, paramName, operationField, paramName); 500 stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '')", operationField, paramName, operationField, paramName);
488 break; 501 break;
489 case NOT_CONTAINS: 502 case NOT_CONTAINS:
490 - if (value.length()>0) { 503 + if (value.length() > 0) {
491 value = "%" + value + "%"; 504 value = "%" + value + "%";
492 } 505 }
493 stringOperationQuery = String.format("%s not like :%s) or (%s is null and :%s != '')", operationField, paramName, operationField, paramName); 506 stringOperationQuery = String.format("%s not like :%s) or (%s is null and :%s != '')", operationField, paramName, operationField, paramName);
@@ -41,8 +41,9 @@ public class QueryContext implements SqlParameterSource { @@ -41,8 +41,9 @@ public class QueryContext implements SqlParameterSource {
41 } 41 }
42 42
43 void addParameter(String name, Object value, int type, String typeName) { 43 void addParameter(String name, Object value, int type, String typeName) {
44 - Parameter existing = params.put(name, new Parameter(value, type, typeName));  
45 - if (existing != null) { 44 + Parameter newParam = new Parameter(value, type, typeName);
  45 + Parameter oldParam = params.put(name, newParam);
  46 + if (oldParam != null && !oldParam.value.equals(newParam.value)) {
46 throw new RuntimeException("Parameter with name: " + name + " was already registered!"); 47 throw new RuntimeException("Parameter with name: " + name + " was already registered!");
47 } 48 }
48 } 49 }
@@ -36,4 +36,6 @@ CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id @@ -36,4 +36,6 @@ CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id
36 36
37 CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); 37 CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
38 38
39 -CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);  
  39 +CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);
  40 +
  41 +CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);