diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index 22fdd77d7a..5b4287acd1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -130,7 +130,7 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperation()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 46ebf31f9d..eff74778dc 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -1737,234 +1737,171 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // --- OR conditions tests --- - @Test - public void testCountEntitiesWithOrKeyFiltersOperation() throws Exception { - // Create 3 devices with different temperature attributes - Device deviceA = new Device(); - deviceA.setName("OrTestDeviceA"); - deviceA.setType("orTestType"); - deviceA = doPost("/api/device", deviceA, Device.class); - String payloadA = "{\"temperature\":60}"; - doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, payloadA, String.class, status().isOk()); - - Device deviceB = new Device(); - deviceB.setName("OrTestDeviceB"); - deviceB.setType("orTestType"); - deviceB = doPost("/api/device", deviceB, Device.class); - String payloadB = "{\"temperature\":5}"; - doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, payloadB, String.class, status().isOk()); - - Device deviceC = new Device(); - deviceC.setName("OrTestDeviceC"); - deviceC.setType("orTestType"); - deviceC = doPost("/api/device", deviceC, Device.class); - String payloadC = "{\"temperature\":30}"; - doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, payloadC, String.class, status().isOk()); + private Device createDeviceWithSharedAttributes(String name, String type, String sharedAttributesPayload) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType(type); + device = doPost("/api/device", device, Device.class); + if (sharedAttributesPayload != null) { + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, + sharedAttributesPayload, String.class, status().isOk()); + } + return device; + } + + private Device createDeviceWithTimeseries(String name, String type, String timeseriesPayload) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType(type); + device = doPost("/api/device", device, Device.class); + JsonNode payload = JacksonUtil.toJsonNode(timeseriesPayload); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", payload) + .andExpect(status().isOk()); + return device; + } + + private Alarm createAlarm(DeviceId originator, String type, AlarmSeverity severity) throws Exception { + Alarm alarm = new Alarm(); + alarm.setOriginator(originator); + alarm.setType(type); + alarm.setSeverity(severity); + return doPost("/api/alarm", alarm, Alarm.class); + } + private static DeviceTypeFilter deviceTypeFilter(String type) { DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orTestType")); + filter.setDeviceTypes(List.of(type)); filter.setDeviceNameFilter(""); + return filter; + } - // Filter 1: temperature > 50 - 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); + private static KeyFilter numericKeyFilter(EntityKeyType keyType, String key, + NumericFilterPredicate.NumericOperation operation, double value) { + KeyFilter keyFilter = new KeyFilter(); + keyFilter.setKey(new EntityKey(keyType, key)); + keyFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(value)); + predicate.setOperation(operation); + keyFilter.setPredicate(predicate); + return keyFilter; + } - // Filter 2: temperature < 10 - 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); + private static KeyFilter numericAttributeKeyFilter(String key, + NumericFilterPredicate.NumericOperation operation, double value) { + return numericKeyFilter(EntityKeyType.ATTRIBUTE, key, operation, value); + } + + private static KeyFilter stringKeyFilter(EntityKeyType keyType, String key, + StringFilterPredicate.StringOperation operation, String value) { + KeyFilter keyFilter = new KeyFilter(); + keyFilter.setKey(new EntityKey(keyType, key)); + keyFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + keyFilter.setPredicate(predicate); + return keyFilter; + } + + private static KeyFilter stringAttributeKeyFilter(String key, + StringFilterPredicate.StringOperation operation, String value) { + return stringKeyFilter(EntityKeyType.ATTRIBUTE, key, operation, value); + } + + private static EntityDataPageLink pageLinkSortedByName(int pageSize, int page, String textSearch) { + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC); + return new EntityDataPageLink(pageSize, page, textSearch, sortOrder); + } + + private static List nameEntityField() { + return Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + } + + private static List extractNames(PageData result) { + return result.getData().stream() + .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) + .collect(Collectors.toList()); + } + + @Test + public void testCountEntitiesWithOrKeyFiltersOperation() throws Exception { + String type = "orTestType"; + createDeviceWithSharedAttributes("OrTestDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrTestDeviceB", type, "{\"temperature\":5}"); + createDeviceWithSharedAttributes("OrTestDeviceC", type, "{\"temperature\":30}"); - List keyFilters = List.of(tempGt50, tempLt10); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); // OR: deviceA (60>50) and deviceB (5<10) match => count=2 - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: no device has temperature both >50 AND <10 => count=0 - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); countByQueryAndCheck(andQuery, 0); } @Test public void testFindEntityDataWithOrKeyFiltersOperation() throws Exception { - // Create devices with different attributes - Device deviceX = new Device(); - deviceX.setName("OrDataDeviceX"); - deviceX.setType("orDataType"); - deviceX = doPost("/api/device", deviceX, Device.class); - doPost("/api/plugins/telemetry/" + deviceX.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"status\":\"active\"}", String.class, status().isOk()); - - Device deviceY = new Device(); - deviceY.setName("OrDataDeviceY"); - deviceY.setType("orDataType"); - deviceY = doPost("/api/device", deviceY, Device.class); - doPost("/api/plugins/telemetry/" + deviceY.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"humidity\":80}", String.class, status().isOk()); - - Device deviceZ = new Device(); - deviceZ.setName("OrDataDeviceZ"); - deviceZ.setType("orDataType"); - deviceZ = doPost("/api/device", deviceZ, Device.class); - // deviceZ has neither matching attribute + String type = "orDataType"; + createDeviceWithSharedAttributes("OrDataDeviceX", type, "{\"status\":\"active\"}"); + createDeviceWithSharedAttributes("OrDataDeviceY", type, "{\"humidity\":80}"); + createDeviceWithSharedAttributes("OrDataDeviceZ", type, null); // no matching attribute - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orDataType")); - filter.setDeviceNameFilter(""); - - // Filter 1: status = "active" - KeyFilter statusFilter = new KeyFilter(); - statusFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "status")); - statusFilter.setValueType(EntityKeyValueType.STRING); - StringFilterPredicate statusPred = new StringFilterPredicate(); - statusPred.setValue(FilterPredicateValue.fromString("active")); - statusPred.setOperation(StringFilterPredicate.StringOperation.EQUAL); - statusFilter.setPredicate(statusPred); - - // Filter 2: humidity > 70 - KeyFilter humidityFilter = new KeyFilter(); - humidityFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity")); - humidityFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate humidityPred = new NumericFilterPredicate(); - humidityPred.setValue(FilterPredicateValue.fromDouble(70)); - humidityPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - humidityFilter.setPredicate(humidityPred); - - List keyFilters = List.of(statusFilter, humidityFilter); - - EntityDataSortOrder sortOrder = new EntityDataSortOrder( - new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC - ); - EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); - List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List keyFilters = List.of( + stringAttributeKeyFilter("status", StringFilterPredicate.StringOperation.EQUAL, "active"), + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 70)); // OR: deviceX matches status=active, deviceY matches humidity>70 - EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + EntityDataQuery orQuery = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); PageData result = findByQueryAndCheck(orQuery, 2); - List names = result.getData().stream() - .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) - .collect(Collectors.toList()); - assertThat(names).containsExactlyInAnyOrder("OrDataDeviceX", "OrDataDeviceY"); + assertThat(extractNames(result)).containsExactlyInAnyOrder("OrDataDeviceX", "OrDataDeviceY"); } @Test public void testFindEntityDataWithOrSameKeyFilters() throws Exception { - // Create devices with temperature values that test same-key OR ungrouping - Device deviceA = new Device(); - deviceA.setName("OrSameKeyDeviceA"); - deviceA.setType("orSameKeyType"); - 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("OrSameKeyDeviceB"); - deviceB.setType("orSameKeyType"); - deviceB = doPost("/api/device", deviceB, Device.class); - doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":5}", String.class, status().isOk()); - - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orSameKeyType")); - filter.setDeviceNameFilter(""); - - // Filter 1: temperature > 50 - 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 (same key, different predicate) - 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 keyFilters = List.of(tempGt50, tempLt10); + String type = "orSameKeyType"; + createDeviceWithSharedAttributes("OrSameKeyDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrSameKeyDeviceB", type, "{\"temperature\":5}"); - EntityDataSortOrder sortOrder = new EntityDataSortOrder( - new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC - ); - EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); - List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + // Same key "temperature" with two different predicates exercises the OR-ungrouping path + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); - // OR on same key: deviceA (60>50) and deviceB (5<10) should both be returned - EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + EntityDataQuery orQuery = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); PageData result = findByQueryAndCheck(orQuery, 2); - List names = result.getData().stream() - .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) - .collect(Collectors.toList()); - assertThat(names).containsExactlyInAnyOrder("OrSameKeyDeviceA", "OrSameKeyDeviceB"); + assertThat(extractNames(result)).containsExactlyInAnyOrder("OrSameKeyDeviceA", "OrSameKeyDeviceB"); } @Test public void testCountEntitiesWithoutKeyFiltersOperation() throws Exception { - // Create 2 devices with temperature attributes - Device deviceA = new Device(); - deviceA.setName("BackCompatDeviceA"); - deviceA.setType("backCompatType"); - 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("BackCompatDeviceB"); - deviceB.setType("backCompatType"); - deviceB = doPost("/api/device", deviceB, Device.class); - doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":5}", String.class, status().isOk()); + String type = "backCompatType"; + createDeviceWithSharedAttributes("BackCompatDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("BackCompatDeviceB", type, "{\"temperature\":5}"); - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("backCompatType")); - filter.setDeviceNameFilter(""); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); - // Filter 1: temperature > 50 - 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 - 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 keyFilters = List.of(tempGt50, tempLt10); - - // Await attribute propagation: verify with an OR query that should find 2 when propagated - EntityCountQuery orCheckQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + // Await attribute propagation via an OR query that should find 2 when propagated + EntityCountQuery orCheckQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orCheckQuery, 2)); // Query without keyFiltersOperation (null) -- should behave as AND - EntityCountQuery nullOpQuery = new EntityCountQuery(filter, keyFilters); + EntityCountQuery nullOpQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters); Long nullResult = countByQueryAndCheck(nullOpQuery, 0); // Query with explicit AND -- should produce the same result - EntityCountQuery andOpQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andOpQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); Long andResult = countByQueryAndCheck(andOpQuery, 0); Assert.assertEquals(nullResult, andResult); @@ -1974,87 +1911,34 @@ public class EntityQueryControllerTest extends AbstractControllerTest { public void testAlarmDataQueryWithOrKeyFiltersOperation() throws Exception { loginTenantAdmin(); - // Create devices with different temperatures and alarms - Device deviceHot = new Device(); - deviceHot.setName("OrAlarmDeviceHot"); - deviceHot.setType("orAlarmType"); - 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("OrAlarmDeviceCold"); - deviceCold.setType("orAlarmType"); - 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("OrAlarmDeviceMid"); - deviceMid.setType("orAlarmType"); - deviceMid = doPost("/api/device", deviceMid, Device.class); - doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":30}", String.class, status().isOk()); - - // Create alarms for each device - Alarm alarmHot = new Alarm(); - alarmHot.setOriginator(deviceHot.getId()); - alarmHot.setType("highTemp"); - alarmHot.setSeverity(AlarmSeverity.CRITICAL); - doPost("/api/alarm", alarmHot, Alarm.class); - - Alarm alarmCold = new Alarm(); - alarmCold.setOriginator(deviceCold.getId()); - alarmCold.setType("lowTemp"); - alarmCold.setSeverity(AlarmSeverity.WARNING); - doPost("/api/alarm", alarmCold, Alarm.class); - - Alarm alarmMid = new Alarm(); - alarmMid.setOriginator(deviceMid.getId()); - alarmMid.setType("normalTemp"); - alarmMid.setSeverity(AlarmSeverity.WARNING); - doPost("/api/alarm", alarmMid, Alarm.class); - - // Filter 1: temperature > 50 - 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); + String type = "orAlarmType"; + Device deviceHot = createDeviceWithSharedAttributes("OrAlarmDeviceHot", type, "{\"temperature\":60}"); + Device deviceCold = createDeviceWithSharedAttributes("OrAlarmDeviceCold", type, "{\"temperature\":5}"); + Device deviceMid = createDeviceWithSharedAttributes("OrAlarmDeviceMid", type, "{\"temperature\":30}"); - // Filter 2: temperature < 10 - 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); + createAlarm(deviceHot.getId(), "highTemp", AlarmSeverity.CRITICAL); + createAlarm(deviceCold.getId(), "lowTemp", AlarmSeverity.WARNING); + createAlarm(deviceMid.getId(), "normalTemp", AlarmSeverity.WARNING); - List keyFilters = List.of(tempGt50, tempLt10); - - DeviceTypeFilter entityFilter = new DeviceTypeFilter(); - entityFilter.setDeviceTypes(List.of("orAlarmType")); - entityFilter.setDeviceNameFilter(""); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); pageLink.setPageSize(100); pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); - List alarmFields = List.of(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); // OR query: should return alarms for deviceHot (60>50) and deviceCold (5<10) = 2 alarms - AlarmDataQuery orAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR); + AlarmDataQuery orAlarmQuery = new AlarmDataQuery(deviceTypeFilter(type), pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findAlarmsByQueryAndCheck(orAlarmQuery, 2)); PageData alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2); List 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); + AlarmDataQuery andAlarmQuery = new AlarmDataQuery(deviceTypeFilter(type), pageLink, null, null, keyFilters, alarmFields, ComplexOperation.AND); findAlarmsByQueryAndCheck(andAlarmQuery, 0); } @@ -2062,232 +1946,86 @@ public class EntityQueryControllerTest extends AbstractControllerTest { 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()); - - // 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); - - 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); + String type = "orAlarmCntType"; + Device deviceHot = createDeviceWithSharedAttributes("OrAlarmCntDeviceHot", type, "{\"temperature\":60}"); + Device deviceCold = createDeviceWithSharedAttributes("OrAlarmCntDeviceCold", type, "{\"temperature\":5}"); + Device deviceMid = createDeviceWithSharedAttributes("OrAlarmCntDeviceMid", type, "{\"temperature\":30}"); - 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); + // 2 alarms for deviceHot, 1 for deviceCold, 1 for deviceMid + createAlarm(deviceHot.getId(), "highTemp1", AlarmSeverity.CRITICAL); + createAlarm(deviceHot.getId(), "highTemp2", AlarmSeverity.CRITICAL); + createAlarm(deviceCold.getId(), "lowTemp", AlarmSeverity.WARNING); + createAlarm(deviceMid.getId(), "normalTemp", AlarmSeverity.WARNING); - List keyFilters = List.of(tempGt50, tempLt10); - - DeviceTypeFilter entityFilter = new DeviceTypeFilter(); - entityFilter.setDeviceTypes(List.of("orAlarmCntType")); - entityFilter.setDeviceNameFilter(""); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); // OR: deviceHot (2 alarms) + deviceCold (1 alarm) match => 3 alarms total - AlarmCountQuery orQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.OR); + AlarmCountQuery orQuery = new AlarmCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countAlarmsByQueryAndCheck(orQuery, 3)); // AND: no device matches both filters => 0 - AlarmCountQuery andQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.AND); + AlarmCountQuery andQuery = new AlarmCountQuery(deviceTypeFilter(type), 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()); - - 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 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); + // Exercises the entity field predicate relocation to middle-layer WHERE under OR + String type = "orMixedType"; + createDeviceWithSharedAttributes("OrMixedAlpha", type, "{\"temperature\":10}"); + createDeviceWithSharedAttributes("OrMixedBeta", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrMixedGamma", type, "{\"temperature\":10}"); + + List keyFilters = List.of( + stringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.CONTAINS, "Alpha"), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50)); + + // OR: Alpha matches name contains "Alpha", Beta matches temp>50 => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> 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); + // AND: only Alpha has name "Alpha" AND temp is 10 (not >50) => count=0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), 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()); + String type = "orStrType"; + createDeviceWithSharedAttributes("OrStrDeviceA", type, "{\"color\":\"red\"}"); + createDeviceWithSharedAttributes("OrStrDeviceB", type, "{\"color\":\"blue\"}"); + createDeviceWithSharedAttributes("OrStrDeviceC", type, "{\"color\":\"green\"}"); - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orStrType")); - filter.setDeviceNameFilter(""); + List keyFilters = List.of( + stringAttributeKeyFilter("color", StringFilterPredicate.StringOperation.EQUAL, "red"), + stringAttributeKeyFilter("color", StringFilterPredicate.StringOperation.EQUAL, "blue")); - // 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 keyFilters = List.of(redFilter, blueFilter); - - // OR: deviceA (red) and deviceB (blue) match => count=2 - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + // OR: red and blue match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: no device is both red AND blue => count=0 - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), 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()); - - 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); + String type = "orSingleType"; + createDeviceWithSharedAttributes("OrSingleDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrSingleDeviceB", type, "{\"temperature\":30}"); - List keyFilters = List.of(tempGt50); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50)); // Single filter with OR should behave identically to AND - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 1)); Long orResult = countByQueryAndCheck(orQuery, 1); - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); Long andResult = countByQueryAndCheck(andQuery, 1); Assert.assertEquals(orResult, andResult); @@ -2295,193 +2033,80 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @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()); + String type = "or3fType"; + createDeviceWithSharedAttributes("Or3fDeviceA", type, "{\"temperature\":60,\"humidity\":50,\"pressure\":1000}"); + createDeviceWithSharedAttributes("Or3fDeviceB", type, "{\"temperature\":20,\"humidity\":90,\"pressure\":1000}"); + createDeviceWithSharedAttributes("Or3fDeviceC", type, "{\"temperature\":20,\"humidity\":50,\"pressure\":1050}"); + createDeviceWithSharedAttributes("Or3fDeviceD", type, "{\"temperature\":20,\"humidity\":50,\"pressure\":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 keyFilters = List.of(tempFilter, humFilter, pressFilter); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 80), + numericAttributeKeyFilter("pressure", NumericFilterPredicate.NumericOperation.GREATER, 1040)); // 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); + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 3)); // AND: no device matches all three => 0 - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), 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) - // Device 1: temp=61, Device 2: temp=62, Device 3: temp=2, Device 4: temp=1, Device 5: temp=25 - 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); - 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()); + // 5 devices, 4 match OR filters; explicit temperatures keep the setup easy to read. + String type = "orPageType"; + int[] temperatures = {61, 62, 2, 1, 25}; // devices 1,2: temp>50; 3,4: temp<10; 5: no match + for (int i = 0; i < temperatures.length; i++) { + createDeviceWithSharedAttributes( + String.format("OrPageDevice%02d", i + 1), type, + "{\"temperature\":" + temperatures[i] + "}"); } - 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 keyFilters = List.of(tempGt50, tempLt10); - - EntityDataSortOrder sortOrder = new EntityDataSortOrder( - new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC - ); - List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); // 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); + EntityDataQuery orQuery1 = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(2, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery1, 4)); PageData 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); + EntityDataQuery orQuery2 = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(2, 1, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); PageData 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 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"); + // All names across both pages should be from the 4 matching devices (01..04) + List allNames = new ArrayList<>(extractNames(page1)); + allNames.addAll(extractNames(page2)); + assertThat(allNames).hasSize(4).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()); - - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orZeroType")); - filter.setDeviceNameFilter(""); - - // Await attribute propagation: verify devices are queryable by a filter that matches - KeyFilter tempGt20 = new KeyFilter(); - tempGt20.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); - tempGt20.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate gt20 = new NumericFilterPredicate(); - gt20.setValue(FilterPredicateValue.fromDouble(20)); - gt20.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - tempGt20.setPredicate(gt20); - EntityCountQuery propagationCheck = new EntityCountQuery(filter, List.of(tempGt20), ComplexOperation.OR); + String type = "orZeroType"; + createDeviceWithSharedAttributes("OrZeroDeviceA", type, "{\"temperature\":30}"); + createDeviceWithSharedAttributes("OrZeroDeviceB", type, "{\"temperature\":40}"); + + // Await attribute propagation via a filter that actually matches both devices + EntityCountQuery propagationCheck = new EntityCountQuery(deviceTypeFilter(type), List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 20)), + ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(propagationCheck, 2)); - // 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 keyFilters = List.of(tempGt50, tempLt10); + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); // OR with no matches: neither filter matches any device => count=0 - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); countByQueryAndCheck(orQuery, 0); } @@ -2491,9 +2116,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { ReflectionTestUtils.setField(baseEntityService, "keyFiltersOrConditionsEnabled", false); try { - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("default")); - filter.setDeviceNameFilter(""); + DeviceTypeFilter filter = deviceTypeFilter("default"); // POST a query with OR operation -- should be rejected with 400 EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR); @@ -2516,172 +2139,66 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Test public void testFindEntityDataWithOrAndTextSearch() throws Exception { - // Create 3 devices: 2 match OR filters, but only 1 also matches textSearch - Device deviceA = new Device(); - deviceA.setName("OrTextAlpha"); - deviceA.setType("orTextType"); - 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("OrTextBeta"); - deviceB.setType("orTextType"); - deviceB = doPost("/api/device", deviceB, Device.class); - doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":5}", String.class, status().isOk()); - - Device deviceC = new Device(); - deviceC.setName("OrTextGamma"); - deviceC.setType("orTextType"); - deviceC = doPost("/api/device", deviceC, Device.class); - doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":30}", String.class, status().isOk()); - - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orTextType")); - filter.setDeviceNameFilter(""); - - // Filter 1: temperature > 50 (matches deviceA) - 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 (matches deviceB) - 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 keyFilters = List.of(tempGt50, tempLt10); + // 3 devices: 2 match OR filters, but only 1 also matches textSearch at a time + String type = "orTextType"; + createDeviceWithSharedAttributes("OrTextAlpha", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrTextBeta", type, "{\"temperature\":5}"); + createDeviceWithSharedAttributes("OrTextGamma", type, "{\"temperature\":30}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR without textSearch: Alpha and Beta match => 2 + EntityDataQuery orQueryNoText = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQueryNoText, 2)); - EntityDataSortOrder sortOrder = new EntityDataSortOrder( - new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC - ); - List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + // OR with textSearch="Alpha": only Alpha matches both OR filter AND text search + EntityDataQuery orQueryWithText = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Alpha"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + assertThat(extractNames(findByQueryAndCheck(orQueryWithText, 1))).containsExactly("OrTextAlpha"); - // OR without textSearch: deviceA and deviceB match => 2 - EntityDataPageLink pageLinkNoText = new EntityDataPageLink(10, 0, null, sortOrder); - EntityDataQuery orQueryNoText = new EntityDataQuery(filter, pageLinkNoText, entityFields, null, keyFilters, ComplexOperation.OR); - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQueryNoText, 2)); + // OR with textSearch="Beta": only Beta matches both OR filter AND text search + EntityDataQuery orQueryBeta = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Beta"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + assertThat(extractNames(findByQueryAndCheck(orQueryBeta, 1))).containsExactly("OrTextBeta"); - // OR with textSearch="Alpha": only deviceA matches both OR filter AND text search - EntityDataPageLink pageLinkWithText = new EntityDataPageLink(10, 0, "Alpha", sortOrder); - EntityDataQuery orQueryWithText = new EntityDataQuery(filter, pageLinkWithText, entityFields, null, keyFilters, ComplexOperation.OR); - PageData result = findByQueryAndCheck(orQueryWithText, 1); - String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - assertThat(name).isEqualTo("OrTextAlpha"); - - // OR with textSearch="Beta": only deviceB matches both OR filter AND text search - EntityDataPageLink pageLinkBeta = new EntityDataPageLink(10, 0, "Beta", sortOrder); - EntityDataQuery orQueryBeta = new EntityDataQuery(filter, pageLinkBeta, entityFields, null, keyFilters, ComplexOperation.OR); - PageData resultBeta = findByQueryAndCheck(orQueryBeta, 1); - String nameBeta = resultBeta.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - assertThat(nameBeta).isEqualTo("OrTextBeta"); - - // OR with textSearch="Gamma": deviceC doesn't match any OR filter => 0 - EntityDataPageLink pageLinkGamma = new EntityDataPageLink(10, 0, "Gamma", sortOrder); - EntityDataQuery orQueryGamma = new EntityDataQuery(filter, pageLinkGamma, entityFields, null, keyFilters, ComplexOperation.OR); + // OR with textSearch="Gamma": Gamma doesn't match any OR filter => 0 + EntityDataQuery orQueryGamma = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Gamma"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); findByQueryAndCheck(orQueryGamma, 0); } @Test public void testCountEntitiesWithOrTimeSeriesKeyFilters() throws Exception { - // Create devices and post time-series telemetry (not attributes) - Device deviceA = new Device(); - deviceA.setName("OrTsDeviceA"); - deviceA.setType("orTsType"); - deviceA = doPost("/api/device", deviceA, Device.class); - JsonNode tsPayloadA = JacksonUtil.toJsonNode("{\"temperature\": 60}"); - doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceA.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadA) - .andExpect(status().isOk()); - - Device deviceB = new Device(); - deviceB.setName("OrTsDeviceB"); - deviceB.setType("orTsType"); - deviceB = doPost("/api/device", deviceB, Device.class); - JsonNode tsPayloadB = JacksonUtil.toJsonNode("{\"temperature\": 5}"); - doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceB.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadB) - .andExpect(status().isOk()); - - Device deviceC = new Device(); - deviceC.setName("OrTsDeviceC"); - deviceC.setType("orTsType"); - deviceC = doPost("/api/device", deviceC, Device.class); - JsonNode tsPayloadC = JacksonUtil.toJsonNode("{\"temperature\": 30}"); - doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceC.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadC) - .andExpect(status().isOk()); + String type = "orTsType"; + createDeviceWithTimeseries("OrTsDeviceA", type, "{\"temperature\":60}"); + createDeviceWithTimeseries("OrTsDeviceB", type, "{\"temperature\":5}"); + createDeviceWithTimeseries("OrTsDeviceC", type, "{\"temperature\":30}"); - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orTsType")); - filter.setDeviceNameFilter(""); - - // Filter 1: TIME_SERIES temperature > 50 - KeyFilter tempGt50 = new KeyFilter(); - tempGt50.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); - tempGt50.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate gt50 = new NumericFilterPredicate(); - gt50.setValue(FilterPredicateValue.fromDouble(50)); - gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - tempGt50.setPredicate(gt50); - - // Filter 2: TIME_SERIES temperature < 10 - KeyFilter tempLt10 = new KeyFilter(); - tempLt10.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); - tempLt10.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate lt10 = new NumericFilterPredicate(); - lt10.setValue(FilterPredicateValue.fromDouble(10)); - lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); - tempLt10.setPredicate(lt10); - - List keyFilters = List.of(tempGt50, tempLt10); + List keyFilters = List.of( + numericKeyFilter(EntityKeyType.TIME_SERIES, "temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericKeyFilter(EntityKeyType.TIME_SERIES, "temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); // OR: deviceA (60>50) and deviceB (5<10) match => count=2 - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: no device has ts temperature both >50 AND <10 => count=0 - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); countByQueryAndCheck(andQuery, 0); } @Test public void testCountEntitiesWithOrComplexFilterPredicate() throws Exception { - // Tests key-level ComplexFilterPredicate combined with query-level OR - Device deviceA = new Device(); - deviceA.setName("OrCplxDeviceA"); - deviceA.setType("orCplxType"); - deviceA = doPost("/api/device", deviceA, Device.class); - doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":65,\"humidity\":50}", String.class, status().isOk()); - - Device deviceB = new Device(); - deviceB.setName("OrCplxDeviceB"); - deviceB.setType("orCplxType"); - deviceB = doPost("/api/device", deviceB, Device.class); - doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":25,\"humidity\":90}", String.class, status().isOk()); - - Device deviceC = new Device(); - deviceC.setName("OrCplxDeviceC"); - deviceC.setType("orCplxType"); - deviceC = doPost("/api/device", deviceC, Device.class); - doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, - "{\"temperature\":25,\"humidity\":50}", String.class, status().isOk()); + // Key-level ComplexFilterPredicate combined with query-level OR + String type = "orCplxType"; + createDeviceWithSharedAttributes("OrCplxDeviceA", type, "{\"temperature\":65,\"humidity\":50}"); + createDeviceWithSharedAttributes("OrCplxDeviceB", type, "{\"temperature\":25,\"humidity\":90}"); + createDeviceWithSharedAttributes("OrCplxDeviceC", type, "{\"temperature\":25,\"humidity\":50}"); - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("orCplxType")); - filter.setDeviceNameFilter(""); - - // Key filter 1: temperature > 50 AND temperature < 70 (complex predicate within key filter) - // Matches deviceA (65) only + // Key filter 1: temperature > 50 AND temperature < 70 (complex predicate within key filter) — matches A only NumericFilterPredicate gt50 = new NumericFilterPredicate(); gt50.setValue(FilterPredicateValue.fromDouble(50)); gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); @@ -2691,30 +2208,22 @@ public class EntityQueryControllerTest extends AbstractControllerTest { ComplexFilterPredicate complexTempPred = new ComplexFilterPredicate(); complexTempPred.setOperation(ComplexOperation.AND); complexTempPred.setPredicates(List.of(gt50, lt70)); - KeyFilter tempComplexFilter = new KeyFilter(); tempComplexFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); tempComplexFilter.setValueType(EntityKeyValueType.NUMERIC); tempComplexFilter.setPredicate(complexTempPred); - // Key filter 2: humidity > 80 (simple predicate) - // Matches deviceB (90) only - KeyFilter humFilter = new KeyFilter(); - humFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity")); - humFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate humGt80 = new NumericFilterPredicate(); - humGt80.setValue(FilterPredicateValue.fromDouble(80)); - humGt80.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - humFilter.setPredicate(humGt80); - - List keyFilters = List.of(tempComplexFilter, humFilter); + // Key filter 2: humidity > 80 (simple predicate) — matches B only + List keyFilters = List.of( + tempComplexFilter, + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 80)); - // Query-level OR: deviceA matches key filter 1, deviceB matches key filter 2 => 2 - EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + // Query-level OR: A matches key filter 1, B matches key filter 2 => 2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // Query-level AND: no device matches both key filters => 0 - EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); countByQueryAndCheck(andQuery, 0); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 7367d43910..740b629045 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -348,7 +348,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateEntityNameQuery((EntityNameFilter) query.getEntityFilter()); } if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { - throw new IncorrectParameterException("OR conditions between key filters are disabled"); + throw new IncorrectParameterException("OR conditions between key filters are disabled by the system administrator."); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 100c733df6..c797d21041 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -288,35 +288,28 @@ public class EntityKeyMapping { } public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType, boolean outerContext) { - if (hasFilter()) { - String keyAlias; - if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) { - if (outerContext) { - // 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; - try { - this.entityKeyColumn = null; - List predicates = keyFilters.stream() - .map(keyFilter -> this.buildKeyQuery(ctx, alias, keyFilter, filterType)) - .collect(Collectors.toList()); - return predicates.stream(); - } finally { - this.entityKeyColumn = savedColumn; - } - } else { - keyAlias = "e"; - } - } else { + if (!hasFilter()) { + return Stream.empty(); + } + String keyAlias; + boolean useAliasDirectly = false; + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) { + if (outerContext) { + // In the middle layer (OR relocation), entity field columns are exposed + // by their alias name from the inner subquery SELECT (e.g., "alias2" from + // "cast(e.name as varchar) as alias2"), so buildSimplePredicateQuery must + // use the alias directly as the field instead of appending entityKeyColumn. keyAlias = alias; + useAliasDirectly = true; + } else { + keyAlias = "e"; } - return keyFilters.stream().map(keyFilter -> - this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType)); } else { - return Stream.empty(); + keyAlias = alias; } + final boolean aliasAsField = useAliasDirectly; + return keyFilters.stream().map(keyFilter -> + this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType, aliasAsField)); } public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType) { @@ -620,22 +613,27 @@ public class EntityKeyMapping { private String buildKeyQuery(SqlQueryContext ctx, String alias, KeyFilter keyFilter, EntityFilterType filterType) { - return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType); + return this.buildKeyQuery(ctx, alias, keyFilter, filterType, false); + } + + private String buildKeyQuery(SqlQueryContext ctx, String alias, KeyFilter keyFilter, + EntityFilterType filterType, boolean useAliasAsField) { + return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType, useAliasAsField); } private String buildPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - KeyFilterPredicate predicate, EntityFilterType filterType) { + KeyFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { if (predicate.getType().equals(FilterPredicateType.COMPLEX)) { - return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType); + return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType, useAliasAsField); } else { - return this.buildSimplePredicateQuery(ctx, alias, key, predicate, filterType); + return this.buildSimplePredicateQuery(ctx, alias, key, predicate, filterType, useAliasAsField); } } private String buildComplexPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - ComplexFilterPredicate predicate, EntityFilterType filterType) { + ComplexFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { String result = predicate.getPredicates().stream() - .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) + .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType, useAliasAsField)) .filter(StringUtils::isNotEmpty) .collect(Collectors.joining(" " + predicate.getOperation().name() + " ")); if (!result.trim().isEmpty()) { @@ -645,9 +643,9 @@ public class EntityKeyMapping { } private String buildSimplePredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - KeyFilterPredicate predicate, EntityFilterType filterType) { + KeyFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { - String field = (getEntityKeyColumn() != null) ? alias + "." + getEntityKeyColumn() : alias; + String field = useAliasAsField || getEntityKeyColumn() == null ? alias : alias + "." + getEntityKeyColumn(); if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); } else if (predicate.getType().equals(FilterPredicateType.STRING)) {