Browse Source

Fix OR key filter SQL generation, propagation, and add comprehensive tests

- Move OR filter predicates to middle-layer WHERE clause where JOIN alias
  table references are visible, fixing BadSqlGrammarException
- Use alias name (e.g. alias2) instead of bare column name for entity
  field predicates in middle-layer WHERE
- Propagate keyFiltersOperation through BaseEntityService optimization
  path and alarm subscription contexts (TbAlarmDataSubCtx, TbAlarmCountSubCtx)
- Replace ThingsboardException with IllegalArgumentException for OR
  disabled validation
- Add AlarmCountQuery 3-arg constructor for test convenience
- Add 8 new integration tests: alarm data/count OR, mixed entity field +
  attribute OR, string attributes, single filter, 3-way OR, pagination,
  and zero-match edge case
pull/15394/head
Viacheslav Klimov 6 months ago
parent
commit
ca08f4b435
Failed to extract signature
  1. 12
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  2. 9
      application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  5. 435
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  6. 4
      common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java
  7. 2
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  8. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  9. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

12
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -107,13 +107,13 @@ public class DefaultEntityQueryService implements EntityQueryService {
private AttributesService attributesService;
@Override
public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException {
public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) {
validateKeyFiltersOperation(query);
return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
}
@Override
public PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException {
public PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) {
validateKeyFiltersOperation(query);
if (query.getKeyFilters() != null) {
resolveDynamicValuesInPredicates(
@ -183,7 +183,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
@Override
public PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException {
public PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) {
validateKeyFiltersOperation(query);
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);
PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(),
@ -210,7 +210,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
@Override
public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException {
public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) {
validateKeyFiltersOperation(query);
if (query.getEntityFilter() != null) {
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);
@ -244,9 +244,9 @@ public class DefaultEntityQueryService implements EntityQueryService {
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault());
}
private void validateKeyFiltersOperation(EntityCountQuery query) throws ThingsboardException {
private void validateKeyFiltersOperation(EntityCountQuery query) {
if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) {
throw new ThingsboardException("OR conditions between key filters are disabled", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
throw new IllegalArgumentException("OR conditions between key filters are disabled");
}
}

9
application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java

@ -17,7 +17,6 @@ package org.thingsboard.server.service.query;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
@ -34,13 +33,13 @@ import java.util.Set;
public interface EntityQueryService {
long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException;
long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query);
PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException;
PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query);
PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException;
PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query);
long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException;
long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query);
ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope);

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java

@ -130,7 +130,7 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQu
private EntityDataQuery buildEntityDataQuery() {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)));
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters(), query.getKeyFiltersOperation());
}
private void resetInvocationCounter() {

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -363,7 +363,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
entitiesSortOrder = sortOrder;
}
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperation());
}
}

435
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -78,6 +78,7 @@ import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
@ -2052,6 +2053,440 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
PageData<AlarmData> alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2);
List<String> alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList());
assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp");
// AND query: no device has temp both >50 AND <10 => 0 alarms
AlarmDataQuery andAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.AND);
findAlarmsByQueryAndCheck(andAlarmQuery, 0);
}
@Test
public void testCountAlarmsByQueryWithOrKeyFiltersOperation() throws Exception {
loginTenantAdmin();
Device deviceHot = new Device();
deviceHot.setName("OrAlarmCntDeviceHot");
deviceHot.setType("orAlarmCntType");
deviceHot = doPost("/api/device", deviceHot, Device.class);
doPost("/api/plugins/telemetry/" + deviceHot.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceCold = new Device();
deviceCold.setName("OrAlarmCntDeviceCold");
deviceCold.setType("orAlarmCntType");
deviceCold = doPost("/api/device", deviceCold, Device.class);
doPost("/api/plugins/telemetry/" + deviceCold.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":5}", String.class, status().isOk());
Device deviceMid = new Device();
deviceMid.setName("OrAlarmCntDeviceMid");
deviceMid.setType("orAlarmCntType");
deviceMid = doPost("/api/device", deviceMid, Device.class);
doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":30}", String.class, status().isOk());
Thread.sleep(500);
// Create 2 alarms for deviceHot, 1 for deviceCold, 1 for deviceMid
Alarm alarm1 = new Alarm();
alarm1.setOriginator(deviceHot.getId());
alarm1.setType("highTemp1");
alarm1.setSeverity(AlarmSeverity.CRITICAL);
doPost("/api/alarm", alarm1, Alarm.class);
Alarm alarm2 = new Alarm();
alarm2.setOriginator(deviceHot.getId());
alarm2.setType("highTemp2");
alarm2.setSeverity(AlarmSeverity.CRITICAL);
doPost("/api/alarm", alarm2, Alarm.class);
Alarm alarm3 = new Alarm();
alarm3.setOriginator(deviceCold.getId());
alarm3.setType("lowTemp");
alarm3.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm3, Alarm.class);
Alarm alarm4 = new Alarm();
alarm4.setOriginator(deviceMid.getId());
alarm4.setType("normalTemp");
alarm4.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm4, Alarm.class);
Thread.sleep(500);
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
DeviceTypeFilter entityFilter = new DeviceTypeFilter();
entityFilter.setDeviceTypes(List.of("orAlarmCntType"));
entityFilter.setDeviceNameFilter("");
// OR: deviceHot (2 alarms) + deviceCold (1 alarm) match => 3 alarms total
AlarmCountQuery orQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.OR);
countAlarmsByQueryAndCheck(orQuery, 3);
// AND: no device matches both filters => 0
AlarmCountQuery andQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.AND);
countAlarmsByQueryAndCheck(andQuery, 0);
}
@Test
public void testCountEntitiesWithOrMixedEntityFieldAndAttribute() throws Exception {
// Tests the entity field predicate relocation to middle-layer WHERE under OR
Device deviceA = new Device();
deviceA.setName("OrMixedAlpha");
deviceA.setType("orMixedType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":10}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrMixedBeta");
deviceB.setType("orMixedType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceC = new Device();
deviceC.setName("OrMixedGamma");
deviceC.setType("orMixedType");
deviceC = doPost("/api/device", deviceC, Device.class);
doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":10}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orMixedType"));
filter.setDeviceNameFilter("");
// Filter 1: entity field name CONTAINS "Alpha"
KeyFilter nameFilter = new KeyFilter();
nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
nameFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate namePred = new StringFilterPredicate();
namePred.setValue(FilterPredicateValue.fromString("Alpha"));
namePred.setOperation(StringFilterPredicate.StringOperation.CONTAINS);
nameFilter.setPredicate(namePred);
// Filter 2: attribute temperature > 50
KeyFilter tempFilter = new KeyFilter();
tempFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate tempPred = new NumericFilterPredicate();
tempPred.setValue(FilterPredicateValue.fromDouble(50));
tempPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempFilter.setPredicate(tempPred);
List<KeyFilter> keyFilters = List.of(nameFilter, tempFilter);
// OR: deviceA matches name contains "Alpha", deviceB matches temp>50 => count=2
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
countByQueryAndCheck(orQuery, 2);
// AND: only deviceA has name "Alpha" AND temp is 10 (not >50) => count=0
EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
countByQueryAndCheck(andQuery, 0);
}
@Test
public void testCountEntitiesWithOrStringAttributes() throws Exception {
Device deviceA = new Device();
deviceA.setName("OrStrDeviceA");
deviceA.setType("orStrType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"color\":\"red\"}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrStrDeviceB");
deviceB.setType("orStrType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"color\":\"blue\"}", String.class, status().isOk());
Device deviceC = new Device();
deviceC.setName("OrStrDeviceC");
deviceC.setType("orStrType");
deviceC = doPost("/api/device", deviceC, Device.class);
doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"color\":\"green\"}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orStrType"));
filter.setDeviceNameFilter("");
// Filter 1: color = "red"
KeyFilter redFilter = new KeyFilter();
redFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "color"));
redFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate redPred = new StringFilterPredicate();
redPred.setValue(FilterPredicateValue.fromString("red"));
redPred.setOperation(StringFilterPredicate.StringOperation.EQUAL);
redFilter.setPredicate(redPred);
// Filter 2: color = "blue"
KeyFilter blueFilter = new KeyFilter();
blueFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "color"));
blueFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate bluePred = new StringFilterPredicate();
bluePred.setValue(FilterPredicateValue.fromString("blue"));
bluePred.setOperation(StringFilterPredicate.StringOperation.EQUAL);
blueFilter.setPredicate(bluePred);
List<KeyFilter> keyFilters = List.of(redFilter, blueFilter);
// OR: deviceA (red) and deviceB (blue) match => count=2
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
countByQueryAndCheck(orQuery, 2);
// AND: no device is both red AND blue => count=0
EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
countByQueryAndCheck(andQuery, 0);
}
@Test
public void testCountEntitiesWithOrSingleFilter() throws Exception {
Device deviceA = new Device();
deviceA.setName("OrSingleDeviceA");
deviceA.setType("orSingleType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrSingleDeviceB");
deviceB.setType("orSingleType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":30}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orSingleType"));
filter.setDeviceNameFilter("");
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
List<KeyFilter> keyFilters = List.of(tempGt50);
// Single filter with OR should behave identically to AND
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
Long orResult = countByQueryAndCheck(orQuery, 1);
EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
Long andResult = countByQueryAndCheck(andQuery, 1);
Assert.assertEquals(orResult, andResult);
}
@Test
public void testCountEntitiesWithOrThreeFilters() throws Exception {
Device deviceA = new Device();
deviceA.setName("Or3fDeviceA");
deviceA.setType("or3fType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":60,\"humidity\":50,\"pressure\":1000}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("Or3fDeviceB");
deviceB.setType("or3fType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":20,\"humidity\":90,\"pressure\":1000}", String.class, status().isOk());
Device deviceC = new Device();
deviceC.setName("Or3fDeviceC");
deviceC.setType("or3fType");
deviceC = doPost("/api/device", deviceC, Device.class);
doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":20,\"humidity\":50,\"pressure\":1050}", String.class, status().isOk());
Device deviceD = new Device();
deviceD.setName("Or3fDeviceD");
deviceD.setType("or3fType");
deviceD = doPost("/api/device", deviceD, Device.class);
doPost("/api/plugins/telemetry/" + deviceD.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":20,\"humidity\":50,\"pressure\":1000}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("or3fType"));
filter.setDeviceNameFilter("");
// Filter 1: temperature > 50
KeyFilter tempFilter = new KeyFilter();
tempFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate tempPred = new NumericFilterPredicate();
tempPred.setValue(FilterPredicateValue.fromDouble(50));
tempPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempFilter.setPredicate(tempPred);
// Filter 2: humidity > 80
KeyFilter humFilter = new KeyFilter();
humFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity"));
humFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate humPred = new NumericFilterPredicate();
humPred.setValue(FilterPredicateValue.fromDouble(80));
humPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
humFilter.setPredicate(humPred);
// Filter 3: pressure > 1040
KeyFilter pressFilter = new KeyFilter();
pressFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "pressure"));
pressFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate pressPred = new NumericFilterPredicate();
pressPred.setValue(FilterPredicateValue.fromDouble(1040));
pressPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
pressFilter.setPredicate(pressPred);
List<KeyFilter> keyFilters = List.of(tempFilter, humFilter, pressFilter);
// OR: A matches temp>50, B matches hum>80, C matches press>1040, D matches none => 3
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
countByQueryAndCheck(orQuery, 3);
// AND: no device matches all three => 0
EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND);
countByQueryAndCheck(andQuery, 0);
}
@Test
public void testFindEntityDataWithOrPagination() throws Exception {
// Create 5 devices, 4 match OR filters (1,2: temp>50; 3,4: temp<10; 5: no match)
for (int i = 1; i <= 5; i++) {
Device device = new Device();
device.setName(String.format("OrPageDevice%02d", i));
device.setType("orPageType");
device = doPost("/api/device", device, Device.class);
// Devices 1,2: temperature > 50. Device 3,4: temperature < 10. Device 5: no match (25).
int temp = (i <= 2) ? 60 + i : (i <= 4) ? 5 - i : 25;
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":" + temp + "}", String.class, status().isOk());
}
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orPageType"));
filter.setDeviceNameFilter("");
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC
);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
// Page 1: pageSize=2, totalElements=4, data.size()=2
EntityDataPageLink pageLink1 = new EntityDataPageLink(2, 0, null, sortOrder);
EntityDataQuery orQuery1 = new EntityDataQuery(filter, pageLink1, entityFields, null, keyFilters, ComplexOperation.OR);
PageData<EntityData> page1 = findByQueryAndCheck(orQuery1, 4);
Assert.assertEquals(2, page1.getData().size());
Assert.assertTrue(page1.hasNext());
// Page 2: remaining 2 of 4
EntityDataPageLink pageLink2 = new EntityDataPageLink(2, 1, null, sortOrder);
EntityDataQuery orQuery2 = new EntityDataQuery(filter, pageLink2, entityFields, null, keyFilters, ComplexOperation.OR);
PageData<EntityData> page2 = findByQueryAndCheck(orQuery2, 4);
Assert.assertEquals(2, page2.getData().size());
Assert.assertFalse(page2.hasNext());
// All names across both pages should be from the 4 matching devices (01,02,03,04)
List<String> allNames = new ArrayList<>();
page1.getData().forEach(e -> allNames.add(e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()));
page2.getData().forEach(e -> allNames.add(e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()));
assertThat(allNames).hasSize(4);
assertThat(allNames).doesNotContain("OrPageDevice05");
}
@Test
public void testCountEntitiesWithOrZeroMatches() throws Exception {
Device deviceA = new Device();
deviceA.setName("OrZeroDeviceA");
deviceA.setType("orZeroType");
deviceA = doPost("/api/device", deviceA, Device.class);
doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":30}", String.class, status().isOk());
Device deviceB = new Device();
deviceB.setName("OrZeroDeviceB");
deviceB.setType("orZeroType");
deviceB = doPost("/api/device", deviceB, Device.class);
doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE,
"{\"temperature\":40}", String.class, status().isOk());
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("orZeroType"));
filter.setDeviceNameFilter("");
// Filter 1: temperature > 50 (no match)
KeyFilter tempGt50 = new KeyFilter();
tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempGt50.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate gt50 = new NumericFilterPredicate();
gt50.setValue(FilterPredicateValue.fromDouble(50));
gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
tempGt50.setPredicate(gt50);
// Filter 2: temperature < 10 (no match)
KeyFilter tempLt10 = new KeyFilter();
tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
tempLt10.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate lt10 = new NumericFilterPredicate();
lt10.setValue(FilterPredicateValue.fromDouble(10));
lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS);
tempLt10.setPredicate(lt10);
List<KeyFilter> keyFilters = List.of(tempGt50, tempLt10);
// OR with no matches: neither filter matches any device => count=0
EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR);
countByQueryAndCheck(orQuery, 0);
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java

@ -51,4 +51,8 @@ public class AlarmCountQuery extends EntityCountQuery {
super(entityFilter, keyFilters);
}
public AlarmCountQuery(EntityFilter entityFilter, List<KeyFilter> keyFilters, ComplexOperation keyFiltersOperation) {
super(entityFilter, keyFilters, keyFiltersOperation);
}
}

2
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -409,7 +409,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
.collect(Collectors.toList());
}
}
EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters());
EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters(), query.getKeyFiltersOperation());
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery);
}

24
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -386,11 +386,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters);
String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr);
// Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner
// Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner
// Place at the middle layer (after JOINs) where alias table references are visible.
// Under AND: place at the outer result layer (existing behavior).
String aliasWhereQuery;
if (isOr) {
String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR);
aliasWhereQuery = combinedFilterQuery.isEmpty() ? "" : " where (" + combinedFilterQuery + ")";
if (!combinedFilterQuery.isEmpty()) {
latestJoinsCnt += " where (" + combinedFilterQuery + ")";
}
aliasWhereQuery = "";
} else {
aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, "");
}
@ -481,18 +486,23 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr);
String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false, isOr);
// Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner
// Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner
// Place at the middle layer (after JOINs) where alias table references are visible.
// Under AND: place at the outer result layer (existing behavior).
String aliasWhereQuery;
if (isOr) {
String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR);
String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch());
aliasWhereQuery = "";
String middleWhere = "";
if (!combinedFilterQuery.isEmpty()) {
aliasWhereQuery = " where (" + combinedFilterQuery + ")";
middleWhere = " where (" + combinedFilterQuery + ")";
}
String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch());
if (!searchTextQuery.isEmpty()) {
aliasWhereQuery += (aliasWhereQuery.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") ";
middleWhere += (middleWhere.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") ";
}
latestJoinsCnt += middleWhere;
latestJoinsData += middleWhere;
aliasWhereQuery = "";
} else {
aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch());
}

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

@ -292,16 +292,14 @@ public class EntityKeyMapping {
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();
// In the middle layer (OR relocation), entity field columns are available
// by their alias name from the inner subquery SELECT (e.g., "alias2" from
// "cast(e.name as varchar) as alias2"). Temporarily null out entityKeyColumn
// so buildSimplePredicateQuery uses the alias directly as the field.
String savedColumn = this.entityKeyColumn;
this.entityKeyColumn = null;
List<String> predicates = keyFilters.stream()
.map(keyFilter -> this.buildKeyQuery(ctx, bareColumn, keyFilter, filterType))
.map(keyFilter -> this.buildKeyQuery(ctx, alias, keyFilter, filterType))
.collect(Collectors.toList());
this.entityKeyColumn = savedColumn;
return predicates.stream();

Loading…
Cancel
Save