Commit 37d3b424f969642e2eb7d76ac6e19afd81e3a58b

Authored by Andrew Shvayka
Committed by GitHub
2 parents 2813fa09 319d47ea

Merge pull request #5364 from smatvienko-tb/relation-query-improvements

Relation query improvements (handle many relations between two entities, break loops, limit max level)
... ... @@ -360,6 +360,7 @@
360 360 </systemPropertyVariables>
361 361 <excludes>
362 362 <exclude>**/sql/*Test.java</exclude>
  363 + <exclude>**/psql/*Test.java</exclude>
363 364 <exclude>**/nosql/*Test.java</exclude>
364 365 </excludes>
365 366 <includes>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.id;
  17 +
  18 +import org.junit.Assert;
  19 +import org.junit.Test;
  20 +
  21 +public class EntityIdTest {
  22 +
  23 + @Test
  24 + public void givenConstantNullUuid_whenCompare_thenToStringEqualsPredefinedUuid() {
  25 + Assert.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString());
  26 + }
  27 +
  28 +}
\ No newline at end of file
... ...
... ... @@ -202,6 +202,11 @@
202 202 <artifactId>spring-boot-starter-data-jpa</artifactId>
203 203 </dependency>
204 204 <dependency>
  205 + <groupId>org.springframework.boot</groupId>
  206 + <artifactId>spring-boot-starter-test</artifactId>
  207 + <scope>test</scope>
  208 + </dependency>
  209 + <dependency>
205 210 <groupId>org.springframework</groupId>
206 211 <artifactId>spring-test</artifactId>
207 212 <scope>test</scope>
... ... @@ -212,6 +217,16 @@
212 217 <scope>test</scope>
213 218 </dependency>
214 219 <dependency>
  220 + <groupId>org.testcontainers</groupId>
  221 + <artifactId>postgresql</artifactId>
  222 + <scope>test</scope>
  223 + </dependency>
  224 + <dependency>
  225 + <groupId>org.testcontainers</groupId>
  226 + <artifactId>jdbc</artifactId>
  227 + <scope>test</scope>
  228 + </dependency>
  229 + <dependency>
215 230 <groupId>org.springframework</groupId>
216 231 <artifactId>spring-context-support</artifactId>
217 232 </dependency>
... ... @@ -239,7 +254,14 @@
239 254 <artifactId>maven-surefire-plugin</artifactId>
240 255 <version>${surfire.version}</version>
241 256 <configuration>
  257 + <excludes>
  258 + <exclude>**/sql/*Test.java</exclude>
  259 + <exclude>**/sql/*/*Test.java</exclude>
  260 + <exclude>**/psql/*Test.java</exclude>
  261 + <exclude>**/nosql/*Test.java</exclude>
  262 + </excludes>
242 263 <includes>
  264 + <include>**/*Test.java</include>
243 265 <include>**/*TestSuite.java</include>
244 266 </includes>
245 267 </configuration>
... ...
... ... @@ -223,6 +223,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
223 223 private static final String SELECT_API_USAGE_STATE = "(select aus.id, aus.created_time, aus.tenant_id, aus.entity_id, " +
224 224 "coalesce((select title from tenant where id = aus.entity_id), (select title from customer where id = aus.entity_id)) as name " +
225 225 "from api_usage_state as aus)";
  226 + static final int MAX_LEVEL_DEFAULT = 50; //This value has to be reasonable small to prevent infinite recursion as early as possible
226 227
227 228 static {
228 229 entityTableMap.put(EntityType.ASSET, "asset");
... ... @@ -239,18 +240,30 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
239 240 public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{
240 241 EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.ASSET, EntityType.DEVICE, EntityType.ENTITY_VIEW};
241 242
242   - private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, relation_type, lvl) AS (" +
243   - " SELECT from_id, from_type, to_id, to_type, relation_type, 1 as lvl" +
244   - " FROM relation" +
  243 + private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, lvl, path) AS (" +
  244 + " SELECT from_id, from_type, to_id, to_type," +
  245 + " 1 as lvl," +
  246 + " ARRAY[$in_id] as path" + // initial path
  247 + " FROM relation " +
245 248 " WHERE $in_id = :relation_root_id and $in_type = :relation_root_type and relation_type_group = 'COMMON'" +
  249 + " GROUP BY from_id, from_type, to_id, to_type, lvl, path" +
246 250 " UNION ALL" +
247   - " SELECT r.from_id, r.from_type, r.to_id, r.to_type, r.relation_type, lvl + 1" +
  251 + " SELECT r.from_id, r.from_type, r.to_id, r.to_type," +
  252 + " (re.lvl + 1) as lvl, " +
  253 + " (re.path || ARRAY[r.$in_id]) as path" +
248 254 " FROM relation r" +
249 255 " INNER JOIN related_entities re ON" +
250 256 " r.$in_id = re.$out_id and r.$in_type = re.$out_type and" +
251   - " relation_type_group = 'COMMON' %s)" +
252   - " SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" +
253   - " from related_entities re" +
  257 + " relation_type_group = 'COMMON' " +
  258 + " AND r.$in_id NOT IN (SELECT * FROM unnest(re.path)) " +
  259 + " %s" +
  260 + " GROUP BY r.from_id, r.from_type, r.to_id, r.to_type, (re.lvl + 1), (re.path || ARRAY[r.$in_id])" +
  261 + " )" +
  262 + " SELECT re.$out_id entity_id, re.$out_type entity_type, max(r_int.lvl) lvl" +
  263 + " from related_entities r_int" +
  264 + " INNER JOIN relation re ON re.from_id = r_int.from_id AND re.from_type = r_int.from_type" +
  265 + " AND re.to_id = r_int.to_id AND re.to_type = r_int.to_type" +
  266 + " AND re.relation_type_group = 'COMMON'" +
254 267 " %s GROUP BY entity_id, entity_type) entity";
255 268 private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from");
256 269 private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to");
... ... @@ -580,7 +593,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
580 593 .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type");
581 594
582 595 notExistsPart.append(")");
583   - whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")";
  596 + whereFilter += " and ( r_int.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")";
584 597 }
585 598 from = String.format(from, lvlFilter, whereFilter);
586 599 String query = "( " + selectFields + from + ")";
... ... @@ -659,7 +672,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
659 672 .append(whereFilter.toString().replaceAll("re\\.", "nr\\."));
660 673
661 674 notExistsPart.append(")");
662   - whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")");
  675 + whereFilter.append(" and ( r_int.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")");
663 676 }
664 677 from = String.format(from, lvlFilter, " WHERE " + whereFilter);
665 678 return "( " + selectFields + from + ")";
... ... @@ -693,8 +706,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
693 706 return whereFilter.toString();
694 707 }
695 708
696   - private String getLvlFilter(int maxLevel) {
697   - return maxLevel > 0 ? ("and lvl <= " + (maxLevel - 1)) : "";
  709 + String getLvlFilter(int maxLevel) {
  710 + return "and re.lvl <= " + (getMaxLevel(maxLevel) - 1);
  711 + }
  712 +
  713 + int getMaxLevel(int maxLevel) {
  714 + return maxLevel > 0 ? maxLevel : MAX_LEVEL_DEFAULT;
698 715 }
699 716
700 717 private String getQueryTemplate(EntitySearchDirection direction) {
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao;
  17 +
  18 +import org.junit.extensions.cpsuite.ClasspathSuite;
  19 +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
  20 +import org.junit.runner.RunWith;
  21 +
  22 +@RunWith(ClasspathSuite.class)
  23 +@ClassnameFilters({
  24 + "org.thingsboard.server.dao.service.psql.*SqlTest",
  25 + "org.thingsboard.server.dao.service.attributes.psql.*SqlTest",
  26 + "org.thingsboard.server.dao.service.event.psql.*SqlTest",
  27 + "org.thingsboard.server.dao.service.timeseries.psql.*SqlTest"
  28 +})
  29 +public class PostgreSqlDaoServiceTestSuite {
  30 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao;
  17 +
  18 +import com.google.common.base.Charsets;
  19 +import com.google.common.io.Resources;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +
  22 +import java.io.IOException;
  23 +import java.net.URL;
  24 +import java.sql.Connection;
  25 +import java.sql.SQLException;
  26 +import java.util.List;
  27 +
  28 +@Slf4j
  29 +public class PostgreSqlInitializer {
  30 +
  31 + private static final List<String> sqlFiles = List.of(
  32 + "sql/schema-ts-psql.sql",
  33 + "sql/schema-entities.sql",
  34 + "sql/schema-entities-idx.sql",
  35 + "sql/system-data.sql",
  36 + "sql/system-test-psql.sql");
  37 + private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql";
  38 +
  39 + public static void initDb(Connection conn) {
  40 + cleanUpDb(conn);
  41 + log.info("initialize Postgres DB...");
  42 + try {
  43 + for (String sqlFile : sqlFiles) {
  44 + URL sqlFileUrl = Resources.getResource(sqlFile);
  45 + String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8);
  46 + conn.createStatement().execute(sql);
  47 + }
  48 + } catch (IOException | SQLException e) {
  49 + throw new RuntimeException("Unable to init the Postgres database. Reason: " + e.getMessage(), e);
  50 + }
  51 + log.info("Postgres DB is initialized!");
  52 + }
  53 +
  54 + private static void cleanUpDb(Connection conn) {
  55 + log.info("clean up Postgres DB...");
  56 + try {
  57 + URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile);
  58 + String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8);
  59 + conn.createStatement().execute(dropAllTablesSql);
  60 + } catch (IOException | SQLException e) {
  61 + throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e);
  62 + }
  63 + }
  64 +}
... ...
... ... @@ -47,7 +47,7 @@ import java.util.stream.Collectors;
47 47
48 48 import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
49 49
50   -public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
  50 +public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest {
51 51
52 52 private IdComparator<DeviceProfile> idComparator = new IdComparator<>();
53 53 private IdComparator<DeviceProfileInfo> deviceProfileInfoIdComparator = new IdComparator<>();
... ...
... ... @@ -17,14 +17,17 @@ package org.thingsboard.server.dao.service;
17 17
18 18 import com.google.common.util.concurrent.Futures;
19 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import lombok.extern.slf4j.Slf4j;
20 21 import org.apache.commons.lang3.RandomStringUtils;
21 22 import org.apache.commons.lang3.RandomUtils;
  23 +import org.hamcrest.Matchers;
22 24 import org.junit.After;
23 25 import org.junit.Assert;
24 26 import org.junit.Before;
25 27 import org.junit.Test;
26 28 import org.springframework.beans.factory.annotation.Autowired;
27 29 import org.springframework.jdbc.core.JdbcTemplate;
  30 +import org.springframework.jdbc.core.ResultSetExtractor;
28 31 import org.thingsboard.server.common.data.DataConstants;
29 32 import org.thingsboard.server.common.data.Device;
30 33 import org.thingsboard.server.common.data.EntityType;
... ... @@ -69,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
69 72 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
70 73 import org.thingsboard.server.dao.attributes.AttributesService;
71 74 import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
  75 +import org.thingsboard.server.dao.sql.relation.RelationRepository;
72 76 import org.thingsboard.server.dao.timeseries.TimeseriesService;
73 77
74 78 import java.util.ArrayList;
... ... @@ -82,9 +86,13 @@ import java.util.stream.Collectors;
82 86 import java.util.stream.Stream;
83 87
84 88 import static org.junit.Assert.assertEquals;
  89 +import static org.hamcrest.MatcherAssert.assertThat;
85 90
  91 +@Slf4j
86 92 public abstract class BaseEntityServiceTest extends AbstractServiceTest {
87 93
  94 + static final int ENTITY_COUNT = 5;
  95 +
88 96 @Autowired
89 97 private AttributesService attributesService;
90 98
... ... @@ -96,6 +104,9 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
96 104 @Autowired
97 105 private JdbcTemplate template;
98 106
  107 + @Autowired
  108 + private RelationRepository relationRepository;
  109 +
99 110 @Before
100 111 public void before() {
101 112 Tenant tenant = new Tenant();
... ... @@ -110,7 +121,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
110 121 tenantService.deleteTenant(tenantId);
111 122 }
112 123
113   -
  124 +
114 125 @Test
115 126 public void testCountEntitiesByQuery() throws InterruptedException {
116 127 List<Device> devices = new ArrayList<>();
... ... @@ -154,12 +165,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
154 165 Assert.assertEquals(0, count);
155 166 }
156 167
157   -
  168 +
158 169 @Test
159 170 public void testCountHierarchicalEntitiesByQuery() throws InterruptedException {
160 171 List<Asset> assets = new ArrayList<>();
161 172 List<Device> devices = new ArrayList<>();
162   - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
  173 + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
163 174
164 175 RelationsQueryFilter filter = new RelationsQueryFilter();
165 176 filter.setRootEntity(tenantId);
... ... @@ -168,7 +179,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
168 179 EntityCountQuery countQuery = new EntityCountQuery(filter);
169 180
170 181 long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
171   - Assert.assertEquals(30, count);
  182 + Assert.assertEquals(31, count); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31)
172 183
173 184 filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
174 185 count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
... ... @@ -304,11 +315,25 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
304 315
305 316 @Test
306 317 public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException {
  318 + doTestHierarchicalFindEntityDataWithAttributesByQuery(0, false);
  319 + }
  320 +
  321 + @Test
  322 + public void testHierarchicalFindEntityDataWithAttributesByQueryWithLevel() throws ExecutionException, InterruptedException {
  323 + doTestHierarchicalFindEntityDataWithAttributesByQuery(2, false);
  324 + }
  325 +
  326 + @Test
  327 + public void testHierarchicalFindEntityDataWithAttributesByQueryWithLastLevelOnly() throws ExecutionException, InterruptedException {
  328 + doTestHierarchicalFindEntityDataWithAttributesByQuery(2, true);
  329 + }
  330 +
  331 + private void doTestHierarchicalFindEntityDataWithAttributesByQuery(final int maxLevel, final boolean fetchLastLevelOnly) throws ExecutionException, InterruptedException {
307 332 List<Asset> assets = new ArrayList<>();
308 333 List<Device> devices = new ArrayList<>();
309 334 List<Long> temperatures = new ArrayList<>();
310 335 List<Long> highTemperatures = new ArrayList<>();
311   - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
  336 + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
312 337
313 338 List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
314 339 for (int i = 0; i < devices.size(); i++) {
... ... @@ -321,6 +346,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
321 346 filter.setRootEntity(tenantId);
322 347 filter.setDirection(EntitySearchDirection.FROM);
323 348 filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
  349 + filter.setMaxLevel(maxLevel);
  350 + filter.setFetchLastLevelOnly(fetchLastLevelOnly);
324 351
325 352 EntityDataSortOrder sortOrder = new EntityDataSortOrder(
326 353 new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
... ... @@ -373,14 +400,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
373 400 deviceService.deleteDevicesByTenantId(tenantId);
374 401 }
375 402
376   -
377 403 @Test
378 404 public void testHierarchicalFindDevicesWithAttributesByQuery() throws ExecutionException, InterruptedException {
379 405 List<Asset> assets = new ArrayList<>();
380 406 List<Device> devices = new ArrayList<>();
381 407 List<Long> temperatures = new ArrayList<>();
382 408 List<Long> highTemperatures = new ArrayList<>();
383   - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
  409 + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
384 410
385 411 List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
386 412 for (int i = 0; i < devices.size(); i++) {
... ... @@ -393,6 +419,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
393 419 filter.setRootEntity(tenantId);
394 420 filter.setDirection(EntitySearchDirection.FROM);
395 421 filter.setRelationType("Contains");
  422 + filter.setMaxLevel(2);
  423 + filter.setFetchLastLevelOnly(true);
396 424
397 425 EntityDataSortOrder sortOrder = new EntityDataSortOrder(
398 426 new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
... ... @@ -446,14 +474,14 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
446 474 deviceService.deleteDevicesByTenantId(tenantId);
447 475 }
448 476
449   -
  477 +
450 478 @Test
451 479 public void testHierarchicalFindAssetsWithAttributesByQuery() throws ExecutionException, InterruptedException {
452 480 List<Asset> assets = new ArrayList<>();
453 481 List<Device> devices = new ArrayList<>();
454 482 List<Long> consumptions = new ArrayList<>();
455 483 List<Long> highConsumptions = new ArrayList<>();
456   - createTestHierarchy(assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
  484 + createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
457 485
458 486 List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
459 487 for (int i = 0; i < assets.size(); i++) {
... ... @@ -518,8 +546,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
518 546 deviceService.deleteDevicesByTenantId(tenantId);
519 547 }
520 548
521   - private void createTestHierarchy(List<Asset> assets, List<Device> devices, List<Long> consumptions, List<Long> highConsumptions, List<Long> temperatures, List<Long> highTemperatures) throws InterruptedException {
522   - for (int i = 0; i < 5; i++) {
  549 + private void createTestHierarchy(TenantId tenantId, List<Asset> assets, List<Device> devices, List<Long> consumptions, List<Long> highConsumptions, List<Long> temperatures, List<Long> highTemperatures) throws InterruptedException {
  550 + for (int i = 0; i < ENTITY_COUNT; i++) {
523 551 Asset asset = new Asset();
524 552 asset.setTenantId(tenantId);
525 553 asset.setName("Asset" + i);
... ... @@ -529,18 +557,19 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
529 557 //TO make sure devices have different created time
530 558 Thread.sleep(1);
531 559 assets.add(asset);
532   - EntityRelation er = new EntityRelation();
533   - er.setFrom(tenantId);
534   - er.setTo(asset.getId());
535   - er.setType("Manages");
536   - er.setTypeGroup(RelationTypeGroup.COMMON);
537   - relationService.saveRelation(tenantId, er);
  560 + createRelation(tenantId, "Manages", tenantId, asset.getId());
538 561 long consumption = (long) (Math.random() * 100);
539 562 consumptions.add(consumption);
540 563 if (consumption > 50) {
541 564 highConsumptions.add(consumption);
542 565 }
543   - for (int j = 0; j < 5; j++) {
  566 +
  567 + //tenant -> asset : one-to-one but many edges
  568 + for (int n = 0; n < ENTITY_COUNT; n++) {
  569 + createRelation(tenantId, "UseCase-" + n, tenantId, asset.getId());
  570 + }
  571 +
  572 + for (int j = 0; j < ENTITY_COUNT; j++) {
544 573 Device device = new Device();
545 574 device.setTenantId(tenantId);
546 575 device.setName("A" + i + "Device" + j);
... ... @@ -550,22 +579,125 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
550 579 //TO make sure devices have different created time
551 580 Thread.sleep(1);
552 581 devices.add(device);
553   - er = new EntityRelation();
554   - er.setFrom(asset.getId());
555   - er.setTo(device.getId());
556   - er.setType("Contains");
557   - er.setTypeGroup(RelationTypeGroup.COMMON);
558   - relationService.saveRelation(tenantId, er);
  582 + createRelation(tenantId, "Contains", asset.getId(), device.getId());
559 583 long temperature = (long) (Math.random() * 100);
560 584 temperatures.add(temperature);
561 585 if (temperature > 45) {
562 586 highTemperatures.add(temperature);
563 587 }
  588 +
  589 + //asset -> device : one-to-one but many edges
  590 + for (int n = 0; n < ENTITY_COUNT; n++) {
  591 + createRelation(tenantId, "UseCase-" + n, asset.getId(), device.getId());
  592 + }
  593 + }
  594 + }
  595 +
  596 + //asset -> device one-to-many shared with other assets
  597 + for (int n = 0; n < devices.size(); n = n + ENTITY_COUNT) {
  598 + createRelation(tenantId, "SharedWithAsset0", assets.get(0).getId(), devices.get(n).getId());
  599 + }
  600 +
  601 + createManyCustomRelationsBetweenTwoNodes(tenantId, "UseCase", assets, devices);
  602 + createHorizontalRingRelations(tenantId, "Ring(Loop)-Ast", assets);
  603 + createLoopRelations(tenantId, "Loop-Tnt-Ast-Dev", tenantId, assets.get(0).getId(), devices.get(0).getId());
  604 + createLoopRelations(tenantId, "Loop-Tnt-Ast", tenantId, assets.get(1).getId());
  605 + createLoopRelations(tenantId, "Loop-Ast-Tnt-Ast", assets.get(2).getId(), tenantId, assets.get(3).getId());
  606 +
  607 + //printAllRelations();
  608 + }
  609 +
  610 + private ResultSetExtractor<List<List<String>>> getListResultSetExtractor() {
  611 + return rs -> {
  612 + List<List<String>> list = new ArrayList<>();
  613 + final int columnCount = rs.getMetaData().getColumnCount();
  614 + List<String> columns = new ArrayList<>(columnCount);
  615 + for (int i = 1; i <= columnCount; i++) {
  616 + columns.add(rs.getMetaData().getColumnName(i));
  617 + }
  618 + list.add(columns);
  619 + while (rs.next()) {
  620 + List<String> data = new ArrayList<>(columnCount);
  621 + for (int i = 1; i <= columnCount; i++) {
  622 + data.add(rs.getString(i));
  623 + }
  624 + list.add(data);
564 625 }
  626 + return list;
  627 + };
  628 + }
  629 +
  630 + /*
  631 + * This useful to reproduce exact data in the PostgreSQL and play around with pgadmin query and analyze tool
  632 + * */
  633 + private void printAllRelations() {
  634 + System.out.println("" +
  635 + "DO\n" +
  636 + "$$\n" +
  637 + " DECLARE\n" +
  638 + " someint integer;\n" +
  639 + " BEGIN\n" +
  640 + " DROP TABLE IF EXISTS relation_test;\n" +
  641 + " CREATE TABLE IF NOT EXISTS relation_test\n" +
  642 + " (\n" +
  643 + " from_id uuid,\n" +
  644 + " from_type varchar(255),\n" +
  645 + " to_id uuid,\n" +
  646 + " to_type varchar(255),\n" +
  647 + " relation_type_group varchar(255),\n" +
  648 + " relation_type varchar(255),\n" +
  649 + " additional_info varchar,\n" +
  650 + " CONSTRAINT relation_test_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)\n" +
  651 + " );");
  652 +
  653 + relationRepository.findAll().forEach(r ->
  654 + System.out.printf("INSERT INTO relation_test (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" +
  655 + " VALUES (%s, %s, %s, %s, %s, %s, %s);\n",
  656 + quote(r.getFromId()), quote(r.getFromType()), quote(r.getToId()), quote(r.getToType()),
  657 + quote(r.getRelationTypeGroup()), quote(r.getRelationType()), quote(r.getAdditionalInfo()))
  658 + );
  659 +
  660 + System.out.println("" +
  661 + " END\n" +
  662 + "$$;");
  663 + }
  664 +
  665 + private String quote(Object s) {
  666 + return s == null ? null : "'" + s + "'";
  667 + }
  668 +
  669 + void createLoopRelations(TenantId tenantId, String type, EntityId... ids) {
  670 + assertThat("ids lenght", ids.length, Matchers.greaterThanOrEqualTo(1));
  671 + //chain all from the head to the tail
  672 + for (int i = 1; i < ids.length; i++) {
  673 + relationService.saveRelation(tenantId, new EntityRelation(ids[i - 1], ids[i], type, RelationTypeGroup.COMMON));
  674 + }
  675 + //chain tail -> head
  676 + relationService.saveRelation(tenantId, new EntityRelation(ids[ids.length - 1], ids[0], type, RelationTypeGroup.COMMON));
  677 + }
  678 +
  679 + void createHorizontalRingRelations(TenantId tenantId, String type, List<Asset> assets) {
  680 + createLoopRelations(tenantId, type, assets.stream().map(Asset::getId).toArray(EntityId[]::new));
  681 + }
  682 +
  683 + void createManyCustomRelationsBetweenTwoNodes(TenantId tenantId, String type, List<Asset> assets, List<Device> devices) {
  684 + for (int i = 1; i <= 5; i++) {
  685 + final String typeI = type + i;
  686 + createOneToManyRelations(tenantId, typeI, tenantId, assets.stream().map(Asset::getId).collect(Collectors.toList()));
  687 + assets.forEach(asset ->
  688 + createOneToManyRelations(tenantId, typeI, asset.getId(), devices.stream().map(Device::getId).collect(Collectors.toList())));
565 689 }
566 690 }
567 691
568   -
  692 + void createOneToManyRelations(TenantId tenantId, String type, EntityId from, List<EntityId> toIds) {
  693 + toIds.forEach(toId -> createRelation(tenantId, type, from, toId));
  694 + }
  695 +
  696 + void createRelation(TenantId tenantId, String type, EntityId from, EntityId toId) {
  697 + relationService.saveRelation(tenantId, new EntityRelation(from, toId, type, RelationTypeGroup.COMMON));
  698 + }
  699 +
  700 +
569 701 @Test
570 702 public void testSimpleFindEntityDataByQuery() throws InterruptedException {
571 703 List<Device> devices = new ArrayList<>();
... ... @@ -871,7 +1003,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
871 1003 }
872 1004
873 1005 @Test
874   - public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException{
  1006 + public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException {
875 1007
876 1008 List<Device> devices = new ArrayList<>();
877 1009 List<Long> temperatures = new ArrayList<>();
... ... @@ -1031,7 +1163,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1031 1163
1032 1164 deviceService.deleteDevicesByTenantId(tenantId);
1033 1165 }
1034   -
  1166 +
1035 1167 @Test
1036 1168 public void testFindEntityDataByQueryWithTimeseries() throws ExecutionException, InterruptedException {
1037 1169
... ... @@ -1122,7 +1254,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1122 1254 }
1123 1255
1124 1256 @Test
1125   - public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException{
  1257 + public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException {
1126 1258
1127 1259 List<Device> devices = new ArrayList<>();
1128 1260 List<String> attributeStrings = new ArrayList<>();
... ... @@ -1142,11 +1274,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1142 1274 devices.add(deviceService.saveDevice(device));
1143 1275 //TO make sure devices have different created time
1144 1276 Thread.sleep(1);
1145   - List<StringFilterPredicate.StringOperation> operationValues= Arrays.asList(StringFilterPredicate.StringOperation.values());
  1277 + List<StringFilterPredicate.StringOperation> operationValues = Arrays.asList(StringFilterPredicate.StringOperation.values());
1146 1278 StringFilterPredicate.StringOperation operation = operationValues.get(new Random().nextInt(operationValues.size()));
1147 1279 String operationName = operation.name();
1148 1280 attributeStrings.add(operationName);
1149   - switch(operation){
  1281 + switch (operation) {
1150 1282 case EQUAL:
1151 1283 equalStrings.add(operationName);
1152 1284 notContainsStrings.add(operationName);
... ... @@ -1302,7 +1434,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1302 1434 }
1303 1435
1304 1436 @Test
1305   - public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException{
  1437 + public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException {
1306 1438
1307 1439 List<Device> devices = new ArrayList<>();
1308 1440
... ... @@ -1419,7 +1551,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1419 1551 }
1420 1552
1421 1553 @Test
1422   - public void testBuildSimplePredicateQueryOperations() throws InterruptedException{
  1554 + public void testBuildSimplePredicateQueryOperations() throws InterruptedException {
1423 1555
1424 1556 List<Device> devices = new ArrayList<>();
1425 1557
... ... @@ -1492,7 +1624,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1492 1624 return loadedEntities;
1493 1625 }
1494 1626
1495   - private List<KeyFilter> createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value){
  1627 + private List<KeyFilter> createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value) {
1496 1628 KeyFilter filter = new KeyFilter();
1497 1629 filter.setKey(new EntityKey(keyType, key));
1498 1630 StringFilterPredicate predicate = new StringFilterPredicate();
... ... @@ -1503,7 +1635,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
1503 1635 return Collections.singletonList(filter);
1504 1636 }
1505 1637
1506   - private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value){
  1638 + private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value) {
1507 1639 KeyFilter filter = new KeyFilter();
1508 1640 filter.setKey(new EntityKey(keyType, key));
1509 1641 NumericFilterPredicate predicate = new NumericFilterPredicate();
... ...
... ... @@ -31,7 +31,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
31 31 import java.util.Arrays;
32 32 import java.util.UUID;
33 33
34   -public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
  34 +public abstract class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
35 35
36 36 @Autowired
37 37 protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
... ...
... ... @@ -43,7 +43,7 @@ import java.util.List;
43 43 import java.util.UUID;
44 44 import java.util.stream.Collectors;
45 45
46   -public class BaseOAuth2ServiceTest extends AbstractServiceTest {
  46 +public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest {
47 47 private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, Collections.emptyList());
48 48
49 49 @Autowired
... ...
... ... @@ -34,7 +34,7 @@ import java.util.Collections;
34 34 import java.util.List;
35 35 import java.util.stream.Collectors;
36 36
37   -public class BaseTenantProfileServiceTest extends AbstractServiceTest {
  37 +public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest {
38 38
39 39 private IdComparator<TenantProfile> idComparator = new IdComparator<>();
40 40 private IdComparator<EntityInfo> tenantProfileInfoIdComparator = new IdComparator<>();
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.service;
  17 +
  18 +import org.springframework.test.context.TestPropertySource;
  19 +
  20 +import java.lang.annotation.Documented;
  21 +import java.lang.annotation.ElementType;
  22 +import java.lang.annotation.Inherited;
  23 +import java.lang.annotation.Retention;
  24 +import java.lang.annotation.RetentionPolicy;
  25 +import java.lang.annotation.Target;
  26 +
  27 +@Target(ElementType.TYPE)
  28 +@Retention(RetentionPolicy.RUNTIME)
  29 +@Inherited
  30 +@Documented
  31 +@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:psql-test.properties"})
  32 +public @interface DaoPostgreSqlTest {
  33 +}
... ...
dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java renamed from dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java
... ... @@ -13,11 +13,11 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
16   -package org.thingsboard.server.dao.service.sql;
  16 +package org.thingsboard.server.dao.service.psql;
17 17
18 18 import org.thingsboard.server.dao.service.BaseEntityServiceTest;
19   -import org.thingsboard.server.dao.service.DaoSqlTest;
  19 +import org.thingsboard.server.dao.service.DaoPostgreSqlTest;
20 20
21   -@DaoSqlTest
22   -public class EntityServiceSqlTest extends BaseEntityServiceTest {
  21 +@DaoPostgreSqlTest
  22 +public class EntityServicePostgreSqlTest extends BaseEntityServiceTest {
23 23 }
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.sql.query;
  17 +
  18 +import org.junit.Test;
  19 +import org.thingsboard.server.common.data.id.CustomerId;
  20 +
  21 +import static org.hamcrest.MatcherAssert.assertThat;
  22 +import static org.hamcrest.Matchers.containsString;
  23 +import static org.hamcrest.Matchers.equalTo;
  24 +import static org.mockito.ArgumentMatchers.anyInt;
  25 +import static org.mockito.BDDMockito.willCallRealMethod;
  26 +import static org.mockito.Mockito.mock;
  27 +
  28 +public class DefaultEntityQueryRepositoryTest {
  29 +
  30 + /*
  31 + * This value has to be reasonable small to prevent infinite recursion as early as possible
  32 + * */
  33 + @Test
  34 + public void givenDefaultMaxLevel_whenStaticConstant_thenEqualsTo() {
  35 + assertThat(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT, equalTo(10));
  36 + }
  37 +
  38 + @Test
  39 + public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() {
  40 + DefaultEntityQueryRepository repo = mock(DefaultEntityQueryRepository.class);
  41 + willCallRealMethod().given(repo).getMaxLevel(anyInt());
  42 + assertThat(repo.getMaxLevel(0), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT));
  43 + assertThat(repo.getMaxLevel(-1), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT));
  44 + assertThat(repo.getMaxLevel(-2), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT));
  45 + assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT));
  46 + }
  47 +
  48 + @Test
  49 + public void givenMaxLevelPositive_whenGetMaxLevel_thenValueTheSame() {
  50 + DefaultEntityQueryRepository repo = mock(DefaultEntityQueryRepository.class);
  51 + willCallRealMethod().given(repo).getMaxLevel(anyInt());
  52 + assertThat(repo.getMaxLevel(1), equalTo(1));
  53 + assertThat(repo.getMaxLevel(2), equalTo(2));
  54 + assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(Integer.MAX_VALUE));
  55 + }
  56 +
  57 +}
... ...
  1 +database.ts.type=sql
  2 +database.ts_latest.type=sql
  3 +sql.ts_inserts_executor_type=fixed
  4 +sql.ts_inserts_fixed_thread_pool_size=200
  5 +sql.ts_key_value_partitioning=MONTHS
  6 +#
  7 +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
  8 +spring.jpa.properties.hibernate.order_by.default_null_ordering=last
  9 +spring.jpa.properties.hibernate.jdbc.log.warnings=false
  10 +spring.jpa.show-sql=false
  11 +spring.jpa.hibernate.ddl-auto=none
  12 +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
  13 +spring.datasource.username=postgres
  14 +spring.datasource.password=postgres
  15 +spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
  16 +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
  17 +#org.postgresql.Driver
  18 +spring.datasource.hikari.maximumPoolSize=50
  19 +service.type=monolith
  20 +#database.ts.type=timescale
  21 +#database.ts.type=sql
  22 +#database.entities.type=sql
  23 +#
  24 +#sql.ts_inserts_executor_type=fixed
  25 +#sql.ts_inserts_fixed_thread_pool_size=200
  26 +#sql.ts_key_value_partitioning=MONTHS
  27 +#
  28 +#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
  29 +#spring.jpa.show-sql=false
  30 +#spring.jpa.hibernate.ddl-auto=none
  31 +#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
  32 +#
  33 +#spring.datasource.username=postgres
  34 +#spring.datasource.password=postgres
  35 +#spring.datasource.url=jdbc:postgresql://localhost:5432/sqltest
  36 +#spring.datasource.driverClassName=org.postgresql.Driver
  37 +#spring.datasource.hikari.maximumPoolSize = 50
  38 +queue.core.pack-processing-timeout=3000
  39 +queue.rule-engine.pack-processing-timeout=3000
  40 +queue.rule-engine.queues[0].name=Main
  41 +queue.rule-engine.queues[0].topic=tb_rule_engine.main
  42 +queue.rule-engine.queues[0].poll-interval=25
  43 +queue.rule-engine.queues[0].partitions=3
  44 +queue.rule-engine.queues[0].pack-processing-timeout=3000
  45 +queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES
  46 +queue.rule-engine.queues[0].submit-strategy.type=BURST
  47 +sql.log_entity_queries=true
... ...
  1 +--PostgreSQL specific truncate to fit constraints
  2 +TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain;
\ No newline at end of file
... ...
... ... @@ -1643,6 +1643,17 @@
1643 1643 <groupId>org.hsqldb</groupId>
1644 1644 <artifactId>hsqldb</artifactId>
1645 1645 <version>${hsqldb.version}</version>
  1646 + </dependency>
  1647 + <dependency>
  1648 + <groupId>org.testcontainers</groupId>
  1649 + <artifactId>postgresql</artifactId>
  1650 + <version>${testcontainers.version}</version>
  1651 + <scope>test</scope>
  1652 + </dependency>
  1653 + <dependency>
  1654 + <groupId>org.testcontainers</groupId>
  1655 + <artifactId>jdbc</artifactId>
  1656 + <version>${testcontainers.version}</version>
1646 1657 <scope>test</scope>
1647 1658 </dependency>
1648 1659 <dependency>
... ...