Browse Source

OR-aware SQL generation in EntityKeyMapping

- Add 4-arg buildQuery overload with ComplexOperation parameter that joins predicates with OR
- Add 3-arg toQueries overload with outerContext flag for bare column names in outer WHERE
- Add forceLeftJoin parameter to toLatestJoin and buildLatestJoins for LEFT JOIN under OR
- Modify prepareKeyMapping to create individual mappings per KeyFilter under OR (no same-key grouping)
- Modify prepareEntityCountKeyMapping with same OR ungrouping logic
- All existing 2-arg/3-arg overloads preserved as backward-compatible delegates
pull/15394/head
Viacheslav Klimov 4 months ago
parent
commit
daa092ebe9
Failed to extract signature
  1. 130
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

130
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexOperation;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
@ -283,8 +284,33 @@ public class EntityKeyMapping {
}
public Stream<String> toQueries(SqlQueryContext ctx, EntityFilterType filterType) {
return toQueries(ctx, filterType, false);
}
public Stream<String> toQueries(SqlQueryContext ctx, EntityFilterType filterType, boolean outerContext) {
if (hasFilter()) {
String keyAlias = (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) ? "e" : alias;
String keyAlias;
if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) {
if (outerContext) {
// In the outer query (OR relocation), entity field columns are available
// as bare column names from the inner subquery SELECT.
// buildSimplePredicateQuery does: alias + "." + entityKeyColumn when entityKeyColumn != null.
// To produce just the bare column name, temporarily null out entityKeyColumn
// so buildSimplePredicateQuery uses alias directly as the field.
String bareColumn = getEntityKeyColumn();
String savedColumn = this.entityKeyColumn;
this.entityKeyColumn = null;
List<String> predicates = keyFilters.stream()
.map(keyFilter -> this.buildKeyQuery(ctx, bareColumn, keyFilter, filterType))
.collect(Collectors.toList());
this.entityKeyColumn = savedColumn;
return predicates.stream();
} else {
keyAlias = "e";
}
} else {
keyAlias = alias;
}
return keyFilters.stream().map(keyFilter ->
this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType));
} else {
@ -293,6 +319,10 @@ public class EntityKeyMapping {
}
public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType) {
return toLatestJoin(ctx, entityFilter, entityType, false);
}
public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, boolean forceLeftJoin) {
String entityTypeStr;
if (entityFilter.getType().equals(EntityFilterType.RELATIONS_QUERY)) {
entityTypeStr = "entities.entity_type";
@ -307,13 +337,13 @@ public class EntityKeyMapping {
filterQuery = " AND (" + filterQuery + ")";
}
if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) {
String join = (hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join";
String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join";
return String.format("%s ts_kv_latest %s ON %s.entity_id=entities.id AND %s.key = (select key_id from key_dictionary where key = :%s_key_id) %s",
join, alias, alias, alias, alias, filterQuery);
} else {
String query;
if (!entityKey.getType().equals(EntityKeyType.ATTRIBUTE)) {
String join = (hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join";
String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join";
query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) ",
join, alias, alias, alias, alias);
int scope;
@ -326,7 +356,7 @@ public class EntityKeyMapping {
}
query = String.format("%s AND %s.attribute_type=%s %s", query, alias, scope, filterQuery);
} else {
String join = (hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL";
String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL";
query = String.format("%s (select * from attribute_kv %s WHERE %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) %s " +
"ORDER BY %s.last_update_ts DESC limit 1) as %s ON true",
join, alias, alias, alias, alias, filterQuery, alias, alias);
@ -351,27 +381,44 @@ public class EntityKeyMapping {
}
public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, List<EntityKeyMapping> latestMappings, boolean countQuery) {
return buildLatestJoins(ctx, entityFilter, entityType, latestMappings, countQuery, false);
}
public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType,
List<EntityKeyMapping> latestMappings, boolean countQuery, boolean forceLeftJoin) {
return latestMappings.stream()
.filter(mapping -> !countQuery || mapping.hasFilter())
.map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType))
.map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType, forceLeftJoin))
.collect(Collectors.joining(" "));
}
public static String buildQuery(SqlQueryContext ctx, List<EntityKeyMapping> mappings, EntityFilterType filterType) {
return buildQuery(ctx, mappings, filterType, ComplexOperation.AND);
}
public static String buildQuery(SqlQueryContext ctx, List<EntityKeyMapping> mappings,
EntityFilterType filterType, ComplexOperation operation) {
String joiner = (operation == ComplexOperation.OR) ? " OR " : " AND ";
return mappings.stream()
.flatMap(mapping -> mapping.toQueries(ctx, filterType))
.flatMap(mapping -> mapping.toQueries(ctx, filterType, operation == ComplexOperation.OR))
.filter(StringUtils::isNotEmpty)
.collect(Collectors.joining(" AND "));
.collect(Collectors.joining(joiner));
}
public static List<EntityKeyMapping> prepareKeyMapping(EntityType entityType, EntityDataQuery query) {
EntityFilterType entityFilterType = query.getEntityFilter().getType();
ComplexOperation operation = query.getKeyFiltersOperationOrDefault();
List<EntityKey> entityFields = query.getEntityFields() != null ? query.getEntityFields() : Collections.emptyList();
List<EntityKey> latestValues = query.getLatestValues() != null ? query.getLatestValues() : Collections.emptyList();
Map<EntityKey, List<KeyFilter>> filters =
query.getKeyFilters() != null ?
query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
Map<EntityKey, List<KeyFilter>> filters;
if (operation == ComplexOperation.OR) {
// Under OR, don't group same-key filters; handle individually below
filters = Collections.emptyMap();
} else {
filters = query.getKeyFilters() != null ?
query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
}
EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder();
EntityKey sortOrderKey = sortOrder != null ? sortOrder.getKey() : null;
int index = 2;
@ -428,14 +475,37 @@ public class EntityKeyMapping {
for (EntityKeyMapping mapping : mappings) {
mapping.setIndex(index);
mapping.setAlias(String.format("alias%s", index));
mapping.setKeyFilters(filters.remove(mapping.entityKey));
if (operation != ComplexOperation.OR) {
// Under AND: assign grouped filters to matching selection mappings
mapping.setKeyFilters(filters.remove(mapping.entityKey));
}
if (mapping.getEntityKey().getType().equals(EntityKeyType.ENTITY_FIELD)) {
index++;
} else {
index += 2;
}
}
if (!filters.isEmpty()) {
if (operation == ComplexOperation.OR && query.getKeyFilters() != null) {
// Under OR: each KeyFilter gets its own EntityKeyMapping (no same-key grouping)
for (KeyFilter keyFilter : query.getKeyFilters()) {
EntityKeyMapping mapping = new EntityKeyMapping();
mapping.setIndex(index);
mapping.setAlias(String.format("alias%s", index));
mapping.setKeyFilters(Collections.singletonList(keyFilter));
EntityKey filterKey = keyFilter.getKey();
mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD));
mapping.setEntityKey(filterKey);
mapping.setEntityKeyColumn(entityType, entityFilterType);
mapping.setSelection(mapping.getEntityKeyColumn() == null);
mappings.add(mapping);
if (filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)) {
index += 1;
} else {
index += 2;
}
}
} else if (!filters.isEmpty()) {
// Under AND: create mappings for filter-only keys not in selection
for (EntityKey filterField : filters.keySet()) {
EntityKeyMapping mapping = new EntityKeyMapping();
mapping.setIndex(index);
@ -464,25 +534,45 @@ public class EntityKeyMapping {
public static List<EntityKeyMapping> prepareEntityCountKeyMapping(EntityCountQuery query) {
EntityType entityType = resolveEntityType(query.getEntityFilter());
EntityFilterType entityFilterType = query.getEntityFilter().getType();
ComplexOperation operation = query.getKeyFiltersOperationOrDefault();
Map<EntityKey, List<KeyFilter>> filters =
query.getKeyFilters() != null ?
query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
int index = 2;
List<EntityKeyMapping> mappings = new ArrayList<>();
if (!filters.isEmpty()) {
for (EntityKey filterField : filters.keySet()) {
if (operation == ComplexOperation.OR && query.getKeyFilters() != null && !query.getKeyFilters().isEmpty()) {
// Under OR: each KeyFilter gets its own mapping (no same-key grouping)
for (KeyFilter keyFilter : query.getKeyFilters()) {
EntityKeyMapping mapping = new EntityKeyMapping();
mapping.setIndex(index);
mapping.setAlias(String.format("alias%s", index));
mapping.setKeyFilters(filters.get(filterField));
mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD));
mapping.setEntityKey(filterField);
mapping.setKeyFilters(Collections.singletonList(keyFilter));
EntityKey filterKey = keyFilter.getKey();
mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD));
mapping.setEntityKey(filterKey);
mapping.setEntityKeyColumn(entityType, entityFilterType);
mapping.setSelection(mapping.getEntityKeyColumn() == null);
mappings.add(mapping);
index += 1;
}
} else {
// Under AND (or null/default): group same-key filters together
Map<EntityKey, List<KeyFilter>> filters =
query.getKeyFilters() != null ?
query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
if (!filters.isEmpty()) {
for (EntityKey filterField : filters.keySet()) {
EntityKeyMapping mapping = new EntityKeyMapping();
mapping.setIndex(index);
mapping.setAlias(String.format("alias%s", index));
mapping.setKeyFilters(filters.get(filterField));
mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD));
mapping.setEntityKey(filterField);
mapping.setEntityKeyColumn(entityType, entityFilterType);
mapping.setSelection(mapping.getEntityKeyColumn() == null);
mappings.add(mapping);
index += 1;
}
}
}
return mappings;

Loading…
Cancel
Save