diff options
Diffstat (limited to 'cps-ri/src')
15 files changed, 431 insertions, 233 deletions
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java index 2ffbb4ae0e..82afc5a818 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java @@ -44,6 +44,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; @@ -89,6 +90,7 @@ public class FragmentEntity implements Serializable { @JoinColumn(name = "anchor_id") private AnchorEntity anchor; + @ToString.Exclude @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Set<FragmentEntity> childFragments; diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java index 2cebfc72c0..162b268d87 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2022 Nordix Foundation. + * Copyright (C) 2020-2023 Nordix Foundation. * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. @@ -131,6 +131,13 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic } @Override + public Collection<Anchor> getAnchors(final String dataspaceName, final Collection<String> schemaSetNames) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + return anchorRepository.findAllByDataspaceAndSchemaSetNameIn(dataspaceEntity, schemaSetNames) + .stream().map(CpsAdminPersistenceServiceImpl::toAnchor).collect(Collectors.toSet()); + } + + @Override public Collection<Anchor> queryAnchors(final String dataspaceName, final Collection<String> inputModuleNames) { try { validateDataspaceAndModuleNames(dataspaceName, inputModuleNames); @@ -157,6 +164,13 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic anchorRepository.delete(anchorEntity); } + @Transactional + @Override + public void deleteAnchors(final String dataspaceName, final Collection<String> anchorNames) { + final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + anchorRepository.deleteAllByDataspaceAndNameIn(dataspaceEntity, anchorNames); + } + private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) { final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName); return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 5b310efd5d..f634008dc6 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ import org.onap.cps.spi.exceptions.ConcurrencyException; import org.onap.cps.spi.exceptions.CpsAdminException; import org.onap.cps.spi.exceptions.CpsPathException; import org.onap.cps.spi.exceptions.DataNodeNotFoundException; +import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch; import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.repository.AnchorRepository; @@ -120,7 +121,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private void addNewChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath, final DataNode newChild) { final FragmentEntity parentFragmentEntity = - getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath); + getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(), parentFragmentEntity.getAnchor(), newChild); @@ -136,7 +137,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private void addChildrenDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> newChildren) { final FragmentEntity parentFragmentEntity = - getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath); + getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size()); try { newChildren.forEach(newChildAsDataNode -> { @@ -249,17 +250,28 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } @Override - public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, - final FetchDescendantsOption fetchDescendantsOption) { - final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath, - fetchDescendantsOption); - return toDataNode(fragmentEntity, fetchDescendantsOption); + public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final String targetXpath = isRootXpath(xpath) ? xpath : CpsPathUtil.getNormalizedXpath(xpath); + final Collection<DataNode> dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName, + Collections.singletonList(targetXpath), fetchDescendantsOption); + if (dataNodes.isEmpty()) { + throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath); + } + return dataNodes; } @Override - public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName, - final Collection<String> xpaths, - final FetchDescendantsOption fetchDescendantsOption) { + public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, + final Collection<String> xpaths, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection<FragmentEntity> fragmentEntities = getFragmentEntities(dataspaceName, anchorName, xpaths); + return toDataNodes(fragmentEntities, fetchDescendantsOption); + } + + private Collection<FragmentEntity> getFragmentEntities(final String dataspaceName, final String anchorName, + final Collection<String> xpaths) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); @@ -271,7 +283,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService try { normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath)); } catch (final PathParsingException e) { - log.warn("Error parsing xpath \"{}\" in getDataNodes: {}", xpath, e.getMessage()); + log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage()); } } final Collection<FragmentEntity> fragmentEntities = @@ -283,17 +295,10 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService fragmentEntities.addAll(FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts)); } - return toDataNodes(fragmentEntities, fetchDescendantsOption); + return fragmentEntities; } - private FragmentEntity getFragmentWithoutDescendantsByXpath(final String dataspaceName, - final String anchorName, - final String xpath) { - return getFragmentByXpath(dataspaceName, anchorName, xpath, FetchDescendantsOption.OMIT_DESCENDANTS); - } - - private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName, - final String xpath, final FetchDescendantsOption fetchDescendantsOption) { + private FragmentEntity getFragmentEntity(final String dataspaceName, final String anchorName, final String xpath) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); final FragmentEntity fragmentEntity; @@ -304,13 +309,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService .stream().findFirst().orElse(null); } else { final String normalizedXpath = getNormalizedXpath(xpath); - if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) { - fragmentEntity = - fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, normalizedXpath); - } else { - fragmentEntity = buildFragmentEntitiesFromFragmentExtracts(anchorEntity, normalizedXpath) - .stream().findFirst().orElse(null); - } + fragmentEntity = + fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, normalizedXpath); } if (fragmentEntity == null) { throw new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath); @@ -486,7 +486,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath, final Map<String, Serializable> updateLeaves) { - final FragmentEntity fragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, xpath); + final FragmentEntity fragmentEntity = getFragmentEntity(dataspaceName, anchorName, xpath); final String currentLeavesAsString = fragmentEntity.getAttributes(); final String mergedLeaves = mergeLeaves(updateLeaves, currentLeavesAsString); fragmentEntity.setAttributes(mergedLeaves); @@ -496,8 +496,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName, final DataNode dataNode) { - final FragmentEntity fragmentEntity = - getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, dataNode.getXpath()); + final FragmentEntity fragmentEntity = getFragmentEntity(dataspaceName, anchorName, dataNode.getXpath()); updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode); try { fragmentRepository.save(fragmentEntity); @@ -509,21 +508,24 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } @Override - public void updateDataNodesAndDescendants(final String dataspaceName, - final String anchorName, - final List<DataNode> dataNodes) { - - final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream() - .collect(Collectors.toMap( - dataNode -> dataNode, - dataNode -> - getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, dataNode.getXpath()))); - dataNodeFragmentEntityMap.forEach( - (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode)); + public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, + final List<DataNode> updatedDataNodes) { + final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream() + .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode)); + + final Collection<String> xpaths = xpathToUpdatedDataNode.keySet(); + final Collection<FragmentEntity> existingFragmentEntities = + getFragmentEntities(dataspaceName, anchorName, xpaths); + + for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) { + final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath()); + updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode); + } + try { - fragmentRepository.saveAll(dataNodeFragmentEntityMap.values()); + fragmentRepository.saveAll(existingFragmentEntities); } catch (final StaleStateException staleStateException) { - retryUpdateDataNodesIndividually(dataspaceName, anchorName, dataNodeFragmentEntityMap.values()); + retryUpdateDataNodesIndividually(dataspaceName, anchorName, existingFragmentEntities); } } @@ -577,8 +579,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Transactional public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> newListElements) { - final FragmentEntity parentEntity = - getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath); + final FragmentEntity parentEntity = getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); final String listElementXpathPrefix = getListElementXpathPrefix(newListElements); final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath = extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix); @@ -607,22 +608,44 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override @Transactional + public void deleteDataNodes(final String dataspaceName, final Collection<String> anchorNames) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final Collection<AnchorEntity> anchorEntities = + anchorRepository.findAllByDataspaceAndNameIn(dataspaceEntity, anchorNames); + fragmentRepository.deleteByAnchorIn(anchorEntities); + } + + @Override + @Transactional public void deleteDataNodes(final String dataspaceName, final String anchorName, final Collection<String> xpathsToDelete) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); - final Collection<String> normalizedXpaths = new ArrayList<>(xpathsToDelete.size()); + final Collection<String> deleteChecklist = new HashSet<>(xpathsToDelete.size()); for (final String xpath : xpathsToDelete) { try { - normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath)); + deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath)); } catch (final PathParsingException e) { - log.debug("Error parsing xpath \"{}\" in deleteDataNodes: {}", xpath, e.getMessage()); + log.debug("Error parsing xpath \"{}\": {}", xpath, e.getMessage()); } } - fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), normalizedXpaths); - fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), normalizedXpaths); + final Collection<String> xpathsToExistingContainers = + fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist); + deleteChecklist.removeAll(xpathsToExistingContainers); + + final Collection<String> xpathsToExistingLists = deleteChecklist.stream() + .filter(xpath -> fragmentRepository.existsByAnchorAndXpathStartsWith(anchorEntity, xpath + "[")) + .collect(Collectors.toList()); + deleteChecklist.removeAll(xpathsToExistingLists); + + if (!deleteChecklist.isEmpty()) { + throw new DataNodeNotFoundExceptionBatch(dataspaceName, anchorName, deleteChecklist); + } + + fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingContainers); + fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingLists); } @Override @@ -652,7 +675,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } else { parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(targetXpath); } - parentFragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath); + parentFragmentEntity = getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); if (CpsPathUtil.isPathToListElement(targetXpath)) { targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath); } else { diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java index 3dbd578c73..46b0fec1c2 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java @@ -47,6 +47,12 @@ public interface AnchorRepository extends JpaRepository<AnchorEntity, Integer> { Collection<AnchorEntity> findAllBySchemaSet(@NotNull SchemaSetEntity schemaSetEntity); + Collection<AnchorEntity> findAllByDataspaceAndNameIn(@NotNull DataspaceEntity dataspaceEntity, + @NotNull Collection<String> anchorNames); + + Collection<AnchorEntity> findAllByDataspaceAndSchemaSetNameIn(@NotNull DataspaceEntity dataspaceEntity, + @NotNull Collection<String> schemaSetNames); + Integer countByDataspace(@NotNull DataspaceEntity dataspaceEntity); @Query(value = "SELECT anchor.* FROM yang_resource\n" @@ -58,4 +64,7 @@ public interface AnchorRepository extends JpaRepository<AnchorEntity, Integer> { + "HAVING COUNT(DISTINCT module_name) = :sizeOfModuleNames", nativeQuery = true) Collection<AnchorEntity> getAnchorsByDataspaceIdAndModuleNames(@Param("dataspaceId") int dataspaceId, @Param("moduleNames") Collection<String> moduleNames, @Param("sizeOfModuleNames") int sizeOfModuleNames); -}
\ No newline at end of file + + void deleteAllByDataspaceAndNameIn(@NotNull DataspaceEntity dataspaceEntity, + @NotNull Collection<String> anchorNames); +} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentNativeRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentNativeRepositoryImpl.java index 0e4d359da5..5c5458a039 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentNativeRepositoryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentNativeRepositoryImpl.java @@ -21,8 +21,11 @@ package org.onap.cps.spi.repository; import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import javax.persistence.Query; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -40,55 +43,55 @@ public class FragmentNativeRepositoryImpl implements FragmentNativeRepository { @PersistenceContext private final EntityManager entityManager; - private final TempTableCreator tempTableCreator; - @Override public void deleteFragmentEntity(final long fragmentEntityId) { entityManager.createNativeQuery( - DROP_FRAGMENT_CONSTRAINT - + ADD_FRAGMENT_CONSTRAINT_WITH_CASCADE - + "DELETE FROM fragment WHERE id = ?;" - + DROP_FRAGMENT_CONSTRAINT - + ADD_ORIGINAL_FRAGMENT_CONSTRAINT) + addFragmentConstraintWithDeleteCascade("DELETE FROM fragment WHERE id = ?")) .setParameter(1, fragmentEntityId) .executeUpdate(); } @Override - // Accept security hotspot as temporary table name in SQL query is created internally, not from user input. - @SuppressWarnings("squid:S2077") public void deleteByAnchorIdAndXpaths(final int anchorId, final Collection<String> xpaths) { - if (!xpaths.isEmpty()) { - final String tempTableName = tempTableCreator.createTemporaryTable("xpathsToDelete", xpaths, "xpath"); - entityManager.createNativeQuery( - DROP_FRAGMENT_CONSTRAINT - + ADD_FRAGMENT_CONSTRAINT_WITH_CASCADE - + "DELETE FROM fragment f USING " + tempTableName + " t" - + " WHERE f.anchor_id = :anchorId AND f.xpath = t.xpath;" - + DROP_FRAGMENT_CONSTRAINT - + ADD_ORIGINAL_FRAGMENT_CONSTRAINT) - .setParameter("anchorId", anchorId) - .executeUpdate(); - } + final String queryString = addFragmentConstraintWithDeleteCascade( + "DELETE FROM fragment f WHERE f.anchor_id = ? AND (f.xpath IN (:parameterPlaceholders))"); + executeUpdateWithAnchorIdAndCollection(queryString, anchorId, xpaths); } @Override - // Accept security hotspot as temporary table name in SQL query is created internally, not from user input. + public void deleteListsByAnchorIdAndXpaths(final int anchorId, final Collection<String> listXpaths) { + final Collection<String> listXpathPatterns = + listXpaths.stream().map(listXpath -> listXpath + "[%").collect(Collectors.toSet()); + final String queryString = addFragmentConstraintWithDeleteCascade( + "DELETE FROM fragment f WHERE f.anchor_id = ? AND (f.xpath LIKE ANY (array[:parameterPlaceholders]))"); + executeUpdateWithAnchorIdAndCollection(queryString, anchorId, listXpathPatterns); + } + + // Accept security hotspot as placeholders in SQL query are created internally, not from user input. @SuppressWarnings("squid:S2077") - public void deleteListsByAnchorIdAndXpaths(final int anchorId, final Collection<String> xpaths) { - if (!xpaths.isEmpty()) { - final String tempTableName = tempTableCreator.createTemporaryTable("xpathsToDelete", xpaths, "xpath"); - entityManager.createNativeQuery( - DROP_FRAGMENT_CONSTRAINT - + ADD_FRAGMENT_CONSTRAINT_WITH_CASCADE - + "DELETE FROM fragment f USING " + tempTableName + " t" - + " WHERE f.anchor_id = :anchorId AND f.xpath LIKE CONCAT(t.xpath, :xpathListPattern);" - + DROP_FRAGMENT_CONSTRAINT - + ADD_ORIGINAL_FRAGMENT_CONSTRAINT) - .setParameter("anchorId", anchorId) - .setParameter("xpathListPattern", "[%%") - .executeUpdate(); + private void executeUpdateWithAnchorIdAndCollection(final String sqlTemplate, final int anchorId, + final Collection<String> collection) { + if (!collection.isEmpty()) { + final String parameterPlaceholders = String.join(",", Collections.nCopies(collection.size(), "?")); + final String queryStringWithParameterPlaceholders = + sqlTemplate.replaceFirst(":parameterPlaceholders\\b", parameterPlaceholders); + + final Query query = entityManager.createNativeQuery(queryStringWithParameterPlaceholders); + query.setParameter(1, anchorId); + int parameterIndex = 2; + for (final String parameterValue : collection) { + query.setParameter(parameterIndex++, parameterValue); + } + query.executeUpdate(); } } + private static String addFragmentConstraintWithDeleteCascade(final String queryString) { + return DROP_FRAGMENT_CONSTRAINT + + ADD_FRAGMENT_CONSTRAINT_WITH_CASCADE + + queryString + ";" + + DROP_FRAGMENT_CONSTRAINT + + ADD_ORIGINAL_FRAGMENT_CONSTRAINT; + } + } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java index 8bdb7d985b..51ebcb4127 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java @@ -100,4 +100,10 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, nativeQuery = true)
List<FragmentExtract> quickFindWithDescendants(@Param("anchorId") int anchorId,
@Param("xpathRegex") String xpathRegex);
+
+ @Query("SELECT f.xpath FROM FragmentEntity f WHERE f.anchor = :anchor AND f.xpath IN :xpaths")
+ List<String> findAllXpathByAnchorAndXpathIn(@Param("anchor") AnchorEntity anchorEntity,
+ @Param("xpaths") Collection<String> xpaths);
+
+ boolean existsByAnchorAndXpathStartsWith(AnchorEntity anchorEntity, String xpath);
}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy index 99d44aac89..28d3bcfa4c 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada * Modifications Copyright (C) 2022 TechMahindra Ltd. @@ -142,7 +142,8 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { where: 'the following data is used' dataspaceName || expectedAnchors DATASPACE_NAME || [Anchor.builder().name(ANCHOR_NAME1).schemaSetName(SCHEMA_SET_NAME1).dataspaceName(DATASPACE_NAME).build(), - Anchor.builder().name(ANCHOR_NAME2).schemaSetName(SCHEMA_SET_NAME2).dataspaceName(DATASPACE_NAME).build()] + Anchor.builder().name(ANCHOR_NAME2).schemaSetName(SCHEMA_SET_NAME2).dataspaceName(DATASPACE_NAME).build(), + Anchor.builder().name(ANCHOR_NAME3).schemaSetName(SCHEMA_SET_NAME2).dataspaceName(DATASPACE_NAME).build()] DATASPACE_WITH_NO_DATA || [] } @@ -179,6 +180,17 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) + def 'Get all anchors associated with multiple schemasets in a dataspace.'() { + when: 'anchors are retrieved by dataspace and schema-sets' + def anchors = objectUnderTest.getAnchors('DATASPACE-001', ['SCHEMA-SET-001', 'SCHEMA-SET-002']) + then: ' the response contains expected anchors' + anchors == Set.of( + new Anchor('ANCHOR-001', 'DATASPACE-001', 'SCHEMA-SET-001'), + new Anchor('ANCHOR-002', 'DATASPACE-001', 'SCHEMA-SET-002'), + new Anchor('ANCHOR-003', 'DATASPACE-001', 'SCHEMA-SET-002')) + } + + @Sql([CLEAR_DATA, SET_DATA]) def 'Delete anchor'() { when: 'delete anchor action is invoked' objectUnderTest.deleteAnchor(DATASPACE_NAME, ANCHOR_NAME2) @@ -198,6 +210,15 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { 'anchor does not exists' | DATASPACE_NAME | 'unknown' || AnchorNotFoundException } + @Sql([CLEAR_DATA, SET_DATA]) + def 'Delete multiple anchors'() { + when: 'delete anchors action is invoked' + objectUnderTest.deleteAnchors(DATASPACE_NAME, ['ANCHOR-002', 'ANCHOR-003']) + then: 'anchors are deleted' + anchorRepository.findById(3002).isEmpty() + anchorRepository.findById(3003).isEmpty() + } + @Sql([CLEAR_DATA, SAMPLE_DATA_FOR_ANCHORS_WITH_MODULES]) def 'Query anchors that have #scenario.'() { when: 'all anchor are retrieved for the given dataspace name and module names' @@ -236,7 +257,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { where: 'the following data is used' scenario | dataspaceName || expectedException | expectedMessageDetails 'dataspace name does not exist' | 'unknown' || DataspaceNotFoundException | 'unknown does not exist' - 'dataspace contains an anchor' | 'DATASPACE-001' || DataspaceInUseException | 'contains 2 anchor(s)' + 'dataspace contains an anchor' | 'DATASPACE-001' || DataspaceInUseException | 'contains 3 anchor(s)' 'dataspace contains schemasets' | 'DATASPACE-003' || DataspaceInUseException | 'contains 1 schemaset(s)' } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index e4c552978d..28916b1c4a 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,11 +54,8 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { static DataNodeBuilder dataNodeBuilder = new DataNodeBuilder() static final String SET_DATA = '/data/fragment.sql' - static int DATASPACE_1001_ID = 1001L - static int ANCHOR_3003_ID = 3003L static long ID_DATA_NODE_WITH_DESCENDANTS = 4001 static String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1' - static String XPATH_DATA_NODE_WITH_LEAVES = '/parent-207' static long DATA_NODE_202_FRAGMENT_ID = 4202L static long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L static long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L @@ -82,15 +79,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { ] @Sql([CLEAR_DATA, SET_DATA]) - def 'Get existing datanode with descendants.'() { - when: 'the node is retrieved by its xpath' - def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) - then: 'the path and prefix are populated correctly' - assert dataNode.xpath == '/parent-1' - and: 'dataNode has no prefix (to be addressed by CPS-1301' - assert dataNode.moduleNamePrefix == null - and: 'the child node has the correct path' - assert dataNode.childDataNodes[0].xpath == '/parent-1/child-1' + def 'Get all datanodes with descendants .'() { + when: 'data nodes are retrieved by their xpath' + def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME1, ['/parent-1'], INCLUDE_ALL_DESCENDANTS) + then: 'same data nodes are returned by getDataNodesForMultipleXpaths method' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) == dataNodes + and: 'the dataNodes have no prefix (to be addressed by CPS-1301)' + assert dataNodes[0].moduleNamePrefix == null } @Sql([CLEAR_DATA, SET_DATA]) @@ -102,11 +97,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)] objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes) then: 'it can be retrieved by its xpath' - def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS) - assert dataNode.xpath == parentXpath + def dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS) + assert dataNode[0].xpath == parentXpath and: 'it has the correct child' - assert dataNode.childDataNodes.size() == 1 - def childDataNode = dataNode.childDataNodes[0] + assert dataNode[0].childDataNodes.size() == 1 + def childDataNode = dataNode[0].childDataNodes[0] assert childDataNode.xpath == childXpath and: 'and its grandchild' assert childDataNode.childDataNodes.size() == 1 @@ -236,18 +231,19 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node by xpath without descendants.'() { - when: 'data node is requested' - def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, - inputXPath, OMIT_DESCENDANTS) - then: 'data node is returned with no descendants' - assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES - and: 'expected leaves' - assert result.childDataNodes.size() == 0 - assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES]) - where: 'the following data is used' + def 'Get all data nodes by single xpath without descendants : #scenario'() { + when: 'data nodes are requested' + def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + [inputXPath], OMIT_DESCENDANTS) + then: 'data nodes under root are returned' + assert result.childDataNodes.size() == 2 + and: 'no descendants of parent nodes are returned' + result.each {assert it.childDataNodes.size() == 0} + and: 'same data nodes are returned when V2 of get Data Nodes API is executed' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + inputXPath, OMIT_DESCENDANTS) == result + where: 'the following xpath is used' scenario | inputXPath - 'some xpath' | '/parent-207' 'root xpath' | '/' 'empty xpath' | '' } @@ -255,51 +251,50 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { @Sql([CLEAR_DATA, SET_DATA]) def 'Cps Path query with syntax error throws a CPS Path Exception.'() { when: 'trying to execute a query with a syntax (parsing) error' - objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS) + objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS) then: 'exception is thrown' - def exceptionThrown = thrown(CpsPathException) - assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'') + def exceptionThrown = thrown(PathParsingException) + assert exceptionThrown.getMessage().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'') } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node by xpath with all descendants.'() { - when: 'data node is requested with all descendants' - def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, - inputXPath, INCLUDE_ALL_DESCENDANTS) - def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result) - then: 'data node is returned with all the descendants populated' - assert mappedResult.size() == 4 + def 'Get all data nodes by single xpath with all descendants : #scenario'() { + when: 'data nodes are requested with all descendants' + def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + [inputXPath], INCLUDE_ALL_DESCENDANTS) + def mappedResult = multipleTreesToFlatMapByXpath(new HashMap<>(), result) + then: 'data nodes are returned with all the descendants populated' + assert mappedResult.size() == 8 assert result.childDataNodes.size() == 2 - assert mappedResult.get('/parent-207/child-001').childDataNodes.size() == 0 - assert mappedResult.get('/parent-207/child-002').childDataNodes.size() == 1 - and: 'extracted leaves maps are matching expected' - mappedResult.forEach( - (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath])) + assert mappedResult.get('/parent-208/child-001').childDataNodes.size() == 0 + assert mappedResult.get('/parent-208/child-002').childDataNodes.size() == 1 + assert mappedResult.get('/parent-209/child-001').childDataNodes.size() == 0 + assert mappedResult.get('/parent-209/child-002').childDataNodes.size() == 1 + and: 'same data nodes are returned when V2 of Get Data Nodes API is executed' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + inputXPath, INCLUDE_ALL_DESCENDANTS) == result where: 'the following data is used' scenario | inputXPath - 'some xpath' | '/parent-207' 'root xpath' | '/' 'empty xpath' | '' } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node error scenario: #scenario.'() { - when: 'attempt to get data node with #scenario' - objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) - then: 'a #expectedException is thrown' + def 'Get data nodes error scenario : #scenario.'() { + when: 'attempt to get data nodes with #scenario' + objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) + then: 'an #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | xpath || expectedException - 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException - 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH' || DataNodeNotFoundException - 'invalid xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException + scenario | dataspaceName | anchorName | xpath || expectedException + 'non existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH' || DataNodeNotFoundException + 'invalid Xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || PathParsingException } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get multiple data nodes by xpath.'() { + def 'Get data nodes for multiple xpaths.'() { when: 'fetch #scenario.' - def results = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS) + def results = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS) then: 'the expected number of data nodes are returned' assert results.size() == expectedResultSize where: 'following parameters were used' @@ -312,6 +307,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { '2 unique nodes with duplicate xpath' | ["/parent-200", "/parent-202", "/parent-200"] || 2 'list element with key (single quote)' | ["/parent-201/child-204[@key='A']"] || 1 'list element with key (double quote)' | ['/parent-201/child-204[@key="A"]'] || 1 + 'whole list (not implemented)' | ["/parent-201/child-204"] || 0 'non-existing xpath' | ["/NO-XPATH"] || 0 'existing and non-existing xpaths' | ["/parent-200", "/NO-XPATH", "/parent-201"] || 2 'invalid xpath' | ["INVALID XPATH"] || 0 @@ -323,9 +319,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get multiple data nodes error scenario: #scenario.'() { + def 'Get data nodes for collection of xpath error scenario : #scenario.'() { when: 'attempt to get data nodes with #scenario' - objectUnderTest.getDataNodes(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS) + objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS) then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' @@ -588,22 +584,34 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { 'whole list' | ['/parent-203/child-204'] || ['/parent-203/child-203'] 'list and element in same list' | ['/parent-203/child-204', '/parent-203/child-204[@key="A"]'] || ['/parent-203/child-203'] 'list element under list element' | ['/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]'] || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"] - 'valid but non-existing xpath' | ['/non-existing', '/parent-203/child-204'] || ['/parent-203/child-203'] 'invalid xpath' | ['INVALID XPATH', '/parent-203/child-204'] || ['/parent-203/child-203'] } @Sql([CLEAR_DATA, SET_DATA]) + def 'Delete multiple data nodes error scenario: #scenario.'() { + when: 'deleting nodes is executed for: #scenario.' + objectUnderTest.deleteDataNodes(dataspaceName, anchorName, targetXpaths) + then: 'a #expectedException is thrown' + thrown(expectedException) + where: 'the following data is used' + scenario | dataspaceName | anchorName | targetXpaths || expectedException + 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | ['/not relevant'] || DataspaceNotFoundException + 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | ['/not relevant'] || AnchorNotFoundException + 'non-existing datanode' | DATASPACE_NAME | ANCHOR_NAME3 | ['/NON-EXISTING-XPATH'] || DataNodeNotFoundException + } + + @Sql([CLEAR_DATA, SET_DATA]) def 'Delete data nodes with "/"-token in list key value: #scenario. (CPS-1409)'() { given: 'a data nodes with list-element child with "/" in index value (and grandchild)' def grandChild = new DataNodeBuilder().withXpath(deleteTestGrandChildXPath).build() def child = new DataNodeBuilder().withXpath(deleteTestChildXpath).withChildDataNodes([grandChild]).build() objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTestParentXPath, child) and: 'number of children before delete is stored' - def numberOfChildrenBeforeDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + def numberOfChildrenBeforeDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size() when: 'target node is deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTarget) then: 'one child has been deleted' - def numberOfChildrenAfterDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + def numberOfChildrenAfterDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size() assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1 where: scenario | deleteTarget | pathToParentOfDeletedNode @@ -634,13 +642,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { and: 'data nodes are deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion) when: 'verify data nodes are removed' - objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS) + objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS) then: thrown(DataNodeNotFoundException) and: 'some related object is not deleted' if (xpathSurvivor!=null) { - dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS) - assert dataNode.xpath == xpathSurvivor + dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS) + assert dataNode[0].xpath == xpathSurvivor } where: 'following parameters were used' scenario | xpathForDeletion || xpathSurvivor @@ -667,11 +675,23 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { @Sql([CLEAR_DATA, SET_DATA]) def 'Delete data node for an anchor.'() { given: 'a data-node exists for an anchor' - assert fragmentsExistInDB(DATASPACE_1001_ID, ANCHOR_3003_ID) + assert fragmentsExistInDB(1001, 3003) when: 'data nodes are deleted ' objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3) then: 'all data-nodes are deleted successfully' - assert !fragmentsExistInDB(DATASPACE_1001_ID, ANCHOR_3003_ID) + assert !fragmentsExistInDB(1001, 3003) + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Delete data node for multiple anchors.'() { + given: 'a data-node exists for an anchor' + assert fragmentsExistInDB(1001, 3001) + assert fragmentsExistInDB(1001, 3003) + when: 'data nodes are deleted ' + objectUnderTest.deleteDataNodes(DATASPACE_NAME, ['ANCHOR-001', 'ANCHOR-003']) + then: 'all data-nodes are deleted successfully' + assert !fragmentsExistInDB(1001, 3001) + assert !fragmentsExistInDB(1001, 3003) } def fragmentsExistInDB(dataSpaceId, anchorId) { @@ -711,6 +731,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { return flatMap } + def static multipleTreesToFlatMapByXpath(Map<String, DataNode> flatMap, Collection<DataNode> dataNodeTrees) { + for (DataNode dataNodeTree: dataNodeTrees){ + flatMap.put(dataNodeTree.xpath, dataNodeTree) + dataNodeTree.childDataNodes + .forEach(childDataNode -> multipleTreesToFlatMapByXpath(flatMap, [childDataNode])) + } + return flatMap + } + def keysToXpaths(parent, Collection keys) { return keys.collect { "${parent}/child-list[@key='${it}']".toString() } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 5cabc85b36..ba42a083ea 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -25,7 +25,6 @@ import org.hibernate.StaleStateException import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.entities.AnchorEntity import org.onap.cps.spi.entities.FragmentEntity -import org.onap.cps.spi.entities.FragmentExtract import org.onap.cps.spi.exceptions.ConcurrencyException import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.DataNode @@ -89,15 +88,17 @@ class CpsDataPersistenceServiceSpec extends Specification { } def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() { - given: 'the system contains and can update one datanode' - def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('/node1', 'OK') - and: 'the system contains two more datanodes that throw an exception while updating' - def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('/node2', 'EXCEPTION') - def dataNode3 = createDataNodeAndMockRepositoryMethodSupportingIt('/node3', 'EXCEPTION') + given: 'the system can update one datanode and has two more datanodes that throw an exception while updating' + def dataNodes = createDataNodesAndMockRepositoryMethodSupportingThem([ + '/node1': 'OK', + '/node2': 'EXCEPTION', + '/node3': 'EXCEPTION']) + and: 'db contains an anchor' + mockAnchorRepository.getByDataspaceAndName(*_) >> new AnchorEntity(id:123) and: 'the batch update will therefore also fail' mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } when: 'attempt batch update data nodes' - objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', [dataNode1, dataNode2, dataNode3]) + objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', dataNodes) then: 'concurrency exception is thrown' def thrown = thrown(ConcurrencyException) assert thrown.message == 'Concurrent Transactions' @@ -112,10 +113,10 @@ class CpsDataPersistenceServiceSpec extends Specification { given: 'the db has a fragment with an attribute property JSON value of #scenario' mockFragmentWithJson("{\"some attribute\": ${dataString}}") when: 'getting the data node represented by this fragment' - def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + def dataNode = objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'the leaf is of the correct value and data type' - def attributeValue = dataNode.leaves.get('some attribute') + def attributeValue = dataNode[0].leaves.get('some attribute') assert attributeValue == expectedValue assert attributeValue.class == expectedDataClass where: 'the following Data Type is passed' @@ -136,7 +137,7 @@ class CpsDataPersistenceServiceSpec extends Specification { given: 'a fragment with invalid JSON' mockFragmentWithJson('{invalid json') when: 'getting the data node represented by this fragment' - objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'a data validation exception is thrown' thrown(DataValidationException) @@ -151,7 +152,7 @@ class CpsDataPersistenceServiceSpec extends Specification { def fragmentEntity2 = new FragmentEntity(xpath: '/xpath2', childFragments: []) mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/xpath1', '/xpath2'] as Set<String>) >> [fragmentEntity1, fragmentEntity2] when: 'getting data nodes for 2 xpaths' - def result = objectUnderTest.getDataNodes('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: '2 data nodes are returned' assert result.size() == 2 } @@ -200,7 +201,9 @@ class CpsDataPersistenceServiceSpec extends Specification { def 'update data node and descendants: #scenario'(){ given: 'mocked responses' - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: []) + mockAnchorRepository.getByDataspaceAndName(_, _) >> new AnchorEntity(id:123) + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, [] as Set) >> [] + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, ['/test/xpath'] as Set) >> [new FragmentEntity(xpath: '/test/xpath', childFragments: [])] when: 'replace data node tree' objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes) then: 'call fragment repository save all method' @@ -212,9 +215,12 @@ class CpsDataPersistenceServiceSpec extends Specification { } def 'update data nodes and descendants'() { - given: 'the fragment repository returns a fragment entity related to the xpath input' - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath1') >> new FragmentEntity(xpath: '/test/xpath1', childFragments: []) - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath2') >> new FragmentEntity(xpath: '/test/xpath2', childFragments: []) + given: 'the fragment repository returns fragment entities related to the xpath inputs' + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ + new FragmentEntity(xpath: '/test/xpath1', childFragments: []), + new FragmentEntity(xpath: '/test/xpath2', childFragments: [])] + and: 'db contains an anchor' + mockAnchorRepository.getByDataspaceAndName(*_) >> new AnchorEntity(id:123) and: 'some data nodes with descendants' def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])]) def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])]) @@ -240,13 +246,30 @@ class CpsDataPersistenceServiceSpec extends Specification { return dataNode } + def createDataNodesAndMockRepositoryMethodSupportingThem(Map<String, String> xpathToScenarioMap) { + def dataNodes = [] + def fragmentEntities = [] + xpathToScenarioMap.each { + def xpath = it.key + def scenario = it.value + def dataNode = new DataNodeBuilder().withXpath(xpath).build() + dataNodes.add(dataNode) + def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: []) + fragmentEntities.add(fragmentEntity) + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity + if ('EXCEPTION' == scenario) { + mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } + } + } + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, xpathToScenarioMap.keySet()) >> fragmentEntities + return dataNodes + } + def mockFragmentWithJson(json) { def anchorEntity = new AnchorEntity(id:123) mockAnchorRepository.getByDataspaceAndName(*_) >> anchorEntity - def mockFragmentExtract = Mock(FragmentExtract) - mockFragmentExtract.getId() >> 456 - mockFragmentExtract.getAttributes() >> json - mockFragmentRepository.findByAnchorIdAndParentXpath(*_) >> [mockFragmentExtract] + def fragmentEntity = new FragmentEntity(xpath: '/parent-01', childFragments: [], attributes: json) + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/parent-01'] as Set<String>) >> [fragmentEntity] } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistencePerfSpecBase.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistencePerfSpecBase.groovy index b67a5cc686..daa774698f 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistencePerfSpecBase.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistencePerfSpecBase.groovy @@ -75,12 +75,18 @@ class CpsPersistencePerfSpecBase extends CpsPersistenceSpecBase { return grandChildren } - def countDataNodes(dataNodes) { - int nodeCount = 1 + def countDataNodes(Collection<DataNode> dataNodes) { + int nodeCount = 0 for (DataNode parent : dataNodes) { - for (DataNode child : parent.childDataNodes) { - nodeCount = nodeCount + (countDataNodes(child)) - } + nodeCount = nodeCount + countDataNodes(parent) + } + return nodeCount + } + + def countDataNodes(DataNode dataNode) { + int nodeCount = 1 + for (DataNode child : dataNode.childDataNodes) { + nodeCount = nodeCount + countDataNodes(child) } return nodeCount } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy index 1ecad4e68c..30ff11b458 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021 Bell Canada. + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -60,13 +61,14 @@ class CpsPersistenceSpecBase extends Specification { static final String CLEAR_DATA = '/data/clear-all.sql' - static final String DATASPACE_NAME = 'DATASPACE-001' - static final String SCHEMA_SET_NAME1 = 'SCHEMA-SET-001' - static final String SCHEMA_SET_NAME2 = 'SCHEMA-SET-002' - static final String ANCHOR_NAME1 = 'ANCHOR-001' - static final String ANCHOR_NAME2 = 'ANCHOR-002' - static final String ANCHOR_NAME3 = 'ANCHOR-003' - static final String ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003' - static final String ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004' - static final String ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005' + static def DATASPACE_NAME = 'DATASPACE-001' + static def SCHEMA_SET_NAME1 = 'SCHEMA-SET-001' + static def SCHEMA_SET_NAME2 = 'SCHEMA-SET-002' + static def ANCHOR_NAME1 = 'ANCHOR-001' + static def ANCHOR_NAME2 = 'ANCHOR-002' + static def ANCHOR_NAME3 = 'ANCHOR-003' + static def ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003' + static def ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004' + static def ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005' + static def ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS = 'ANCHOR-006' } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServiceDeletePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServiceDeletePerfTest.groovy index 3b9338ce41..eb138b98be 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServiceDeletePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServiceDeletePerfTest.groovy @@ -24,15 +24,12 @@ import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.impl.CpsPersistencePerfSpecBase import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql -import org.springframework.util.StopWatch class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase { @Autowired CpsDataPersistenceService objectUnderTest - def stopWatch = new StopWatch() - @Sql([CLEAR_DATA, PERF_TEST_DATA]) def 'Create a node with many descendants (please note, subsequent tests depend on this running first).'() { when: 'a node with a large number of descendants is created' @@ -53,8 +50,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase } stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Delete 5 children', 350, deleteDurationInMillis) + then: 'delete duration is under 300 milliseconds' + recordAndAssertPerformance('Delete 5 children', 300, deleteDurationInMillis) } def 'Batch delete 100 children with grandchildren'() { @@ -67,8 +64,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToDelete) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Batch delete 100 children', 350, deleteDurationInMillis) + then: 'delete duration is under 250 milliseconds' + recordAndAssertPerformance('Batch delete 100 children', 250, deleteDurationInMillis) } def 'Delete 50 grandchildren (that have no descendants)'() { @@ -80,8 +77,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase } stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Delete 50 grandchildren', 350, deleteDurationInMillis) + then: 'delete duration is under 300 milliseconds' + recordAndAssertPerformance('Delete 50 grandchildren', 300, deleteDurationInMillis) } def 'Batch delete 500 grandchildren (that have no descendants)'() { @@ -97,8 +94,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToDelete) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Batch delete 500 grandchildren', 350, deleteDurationInMillis) + then: 'delete duration is under 75 milliseconds' + recordAndAssertPerformance('Batch delete 500 grandchildren', 75, deleteDurationInMillis) } @Sql([CLEAR_DATA, PERF_TEST_DATA]) @@ -108,8 +105,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase createLineage(objectUnderTest, 150, 50, true) stopWatch.stop() def setupDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'setup duration is under 10 seconds' - recordAndAssertPerformance('Setup lists', 10_000, setupDurationInMillis) + then: 'setup duration is under 5 seconds' + recordAndAssertPerformance('Setup lists', 5_000, setupDurationInMillis) } def 'Delete 5 whole lists'() { @@ -121,8 +118,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase } stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 1500 milliseconds' - recordAndAssertPerformance('Delete 5 whole lists', 1500, deleteDurationInMillis) + then: 'delete duration is under 1300 milliseconds' + recordAndAssertPerformance('Delete 5 whole lists', 1300, deleteDurationInMillis) } def 'Batch delete 100 whole lists'() { @@ -135,8 +132,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToDelete) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Batch delete 100 whole lists', 350, deleteDurationInMillis) + then: 'delete duration is under 500 milliseconds' + recordAndAssertPerformance('Batch delete 100 whole lists', 500, deleteDurationInMillis) } def 'Delete 10 list elements'() { @@ -148,8 +145,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase } stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 750 milliseconds' - recordAndAssertPerformance('Delete 10 lists elements', 750, deleteDurationInMillis) + then: 'delete duration is under 600 milliseconds' + recordAndAssertPerformance('Delete 10 lists elements', 600, deleteDurationInMillis) } def 'Batch delete 500 list elements'() { @@ -165,8 +162,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToDelete) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 350 milliseconds' - recordAndAssertPerformance('Batch delete 500 lists elements', 350, deleteDurationInMillis) + then: 'delete duration is under 60 milliseconds' + recordAndAssertPerformance('Batch delete 500 lists elements', 60, deleteDurationInMillis) } @Sql([CLEAR_DATA, PERF_TEST_DATA]) @@ -193,8 +190,8 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR, [PERF_TEST_PARENT]) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 300 milliseconds' - recordAndAssertPerformance('Batch delete one large node', 300, deleteDurationInMillis) + then: 'delete duration is under 200 milliseconds' + recordAndAssertPerformance('Batch delete one large node', 200, deleteDurationInMillis) } @Sql([CLEAR_DATA, PERF_TEST_DATA]) @@ -207,12 +204,12 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNode(PERF_DATASPACE, PERF_ANCHOR, '/') stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 300 milliseconds' - recordAndAssertPerformance('Delete root node', 300, deleteDurationInMillis) + then: 'delete duration is under 250 milliseconds' + recordAndAssertPerformance('Delete root node', 250, deleteDurationInMillis) } @Sql([CLEAR_DATA, PERF_TEST_DATA]) - def 'Delete data nodes for an anchor'() {212 + def 'Delete data nodes for an anchor'() { given: 'a node with a large number of descendants is created' createLineage(objectUnderTest, 50, 50, false) createLineage(objectUnderTest, 50, 50, true) @@ -221,8 +218,22 @@ class CpsDataPersistenceServiceDeletePerfTest extends CpsPersistencePerfSpecBase objectUnderTest.deleteDataNodes(PERF_DATASPACE, PERF_ANCHOR) stopWatch.stop() def deleteDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'delete duration is under 300 milliseconds' - recordAndAssertPerformance('Delete data nodes for anchor', 300, deleteDurationInMillis) + then: 'delete duration is under 250 milliseconds' + recordAndAssertPerformance('Delete data nodes for anchor', 250, deleteDurationInMillis) + } + + @Sql([CLEAR_DATA, PERF_TEST_DATA]) + def 'Delete data nodes for multiple anchors'() { + given: 'a node with a large number of descendants is created' + createLineage(objectUnderTest, 50, 50, false) + createLineage(objectUnderTest, 50, 50, true) + when: 'data nodes are deleted' + stopWatch.start() + objectUnderTest.deleteDataNodes(PERF_DATASPACE, [PERF_ANCHOR]) + stopWatch.stop() + def deleteDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'delete duration is under 250 milliseconds' + recordAndAssertPerformance('Delete data nodes for anchors', 250, deleteDurationInMillis) } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy index 0c4f5ec41e..3562419c05 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy @@ -21,7 +21,6 @@ package org.onap.cps.spi.performance import org.onap.cps.spi.impl.CpsPersistencePerfSpecBase -import org.springframework.util.StopWatch import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.repository.AnchorRepository import org.onap.cps.spi.repository.DataspaceRepository @@ -64,17 +63,17 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { def 'Get data node with many descendants by xpath #scenario'() { when: 'get parent is executed with all descendants' stopWatch.start() - def result = objectUnderTest.getDataNode(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'read duration is under 500 milliseconds' - recordAndAssertPerformance("Get ${scenario}", 500, readDurationInMillis) + then: 'read duration is under #allowedDuration milliseconds' + recordAndAssertPerformance("Get ${scenario}", allowedDuration, readDurationInMillis) and: 'data node is returned with all the descendants populated' - assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES + assert countDataNodes(result[0]) == TOTAL_NUMBER_OF_NODES where: 'the following xPaths are used' - scenario || xpath - 'parent' || PERF_TEST_PARENT - 'root' || '' + scenario | xpath || allowedDuration + 'parent' | PERF_TEST_PARENT || 3500 + 'root' | '' || 500 } def 'Query parent data node with many descendants by cps-path'() { @@ -93,13 +92,13 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { when: 'we query for all grandchildren (except 1 for fun) with the new native method' xpathsToAllGrandChildren.remove(0) stopWatch.start() - def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodesForMultipleXpaths(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS) stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'the returned number of entities equal to the number of children * number of grandchildren' assert result.size() == xpathsToAllGrandChildren.size() - and: 'it took less then 4000ms' - recordAndAssertPerformance('Find multiple xpaths', 4000, readDurationInMillis) + and: 'it took less then 3000ms' + recordAndAssertPerformance('Find multiple xpaths', 3000, readDurationInMillis) } def 'Query many descendants by cps-path with #scenario'() { @@ -109,7 +108,6 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'read duration is under #allowedDuration milliseconds' - assert readDurationInMillis < allowedDuration recordAndAssertPerformance("Query many descendants by cpspath (${scenario})", allowedDuration, readDurationInMillis) and: 'data node is returned with all the descendants populated' assert result.size() == NUMBER_OF_CHILDREN @@ -118,4 +116,43 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { 'omit descendants ' | OMIT_DESCENDANTS || 150 'include descendants (although there are none)' | INCLUDE_ALL_DESCENDANTS || 150 } + + def 'Update data nodes with descendants'() { + given: 'a list of xpaths to data nodes with descendants (xpath for each child)' + def xpaths = (1..20).collect { + "${PERF_TEST_PARENT}/perf-test-child-${it}".toString() + } + and: 'the correct number of data nodes are fetched' + def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(PERF_DATASPACE, PERF_ANCHOR, xpaths, INCLUDE_ALL_DESCENDANTS) + assert dataNodes.size() == 20 + assert countDataNodes(dataNodes) == 20 + 20 * 50 + when: 'the fragment entities are updated by the data nodes' + stopWatch.start() + objectUnderTest.updateDataNodesAndDescendants(PERF_DATASPACE, PERF_ANCHOR, dataNodes) + stopWatch.stop() + def updateDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'update duration is under 600 milliseconds' + recordAndAssertPerformance('Update data nodes with descendants', 600, updateDurationInMillis) + } + + def 'Update data nodes without descendants'() { + given: 'a list of xpaths to data nodes without descendants (xpath for each grandchild)' + def xpaths = [] + for (int childIndex = 21; childIndex <= 40; childIndex++) { + xpaths.addAll((1..50).collect { + "${PERF_TEST_PARENT}/perf-test-child-${childIndex}/perf-test-grand-child-${it}".toString() + }) + } + and: 'the correct number of data nodes are fetched' + def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(PERF_DATASPACE, PERF_ANCHOR, xpaths, OMIT_DESCENDANTS) + assert dataNodes.size() == 20 * 50 + assert countDataNodes(dataNodes) == 20 * 50 + when: 'the fragment entities are updated by the data nodes' + stopWatch.start() + objectUnderTest.updateDataNodesAndDescendants(PERF_DATASPACE, PERF_ANCHOR, dataNodes) + stopWatch.stop() + def updateDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'update duration is under 600 milliseconds' + recordAndAssertPerformance('Update data nodes without descendants', 600, updateDurationInMillis) + } } diff --git a/cps-ri/src/test/resources/data/anchor.sql b/cps-ri/src/test/resources/data/anchor.sql index 40fc44c0ae..2ab7966e18 100644 --- a/cps-ri/src/test/resources/data/anchor.sql +++ b/cps-ri/src/test/resources/data/anchor.sql @@ -1,7 +1,7 @@ /* ============LICENSE_START======================================================= Copyright (C) 2020 Pantheon.tech - Modifications Copyright (C) 2020 Nordix Foundation. + Modifications Copyright (C) 2020-2023 Nordix Foundation. Modifications Copyright (C) 2021-2022 Bell Canada. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,8 @@ INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3001, 'ANCHOR-001', 1001, 2001), - (3002, 'ANCHOR-002', 1001, 2002); + (3002, 'ANCHOR-002', 1001, 2002), + (3003, 'ANCHOR-003', 1001, 2002); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES (4001, 1001, 3001, null, '/xpath', '{}'); diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index ad463cffd5..caafcd320a 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -52,7 +52,8 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3001, 'ANCHOR-001', 1001, 2001), (3003, 'ANCHOR-003', 1001, 2001), (3004, 'ncmp-dmi-registry', 1002, 2001), - (3005, 'ANCHOR-005', 1001, 2001); + (3005, 'ANCHOR-005', 1001, 2001), + (3006, 'ANCHOR-006', 1001, 2001); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES (4001, 1001, 3001, null, '/parent-1'), @@ -69,6 +70,16 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (5012, 1001, 3005, 5011, '/parent-207/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (5013, 1001, 3006, null, '/parent-208', '{"parent-leaf-1": "parent-leaf value-1"}'), + (5014, 1001, 3006, 5013, '/parent-208/child-001', '{"first-child-leaf": "first-child-leaf value"}'), + (5015, 1001, 3006, 5013, '/parent-208/child-002', '{"second-child-leaf": "second-child-leaf value"}'), + (5016, 1001, 3006, 5015, '/parent-208/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'), + (5017, 1001, 3006, null, '/parent-209', '{"parent-leaf-2": "parent-leaf value-2"}'), + (5018, 1001, 3006, 5017, '/parent-209/child-001', '{"first-child-leaf": "first-child-leaf value"}'), + (5019, 1001, 3006, 5017, '/parent-209/child-002', '{"second-child-leaf": "second-child-leaf value"}'), + (5020, 1001, 3006, 5019, '/parent-209/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'); + +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES (4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'), (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'), (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'), |