@ -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 ;