Fix issues related to query across all anchors 71/134271/1
authorrajesh.kumar <rk00747546@techmahindra.com>
Fri, 21 Apr 2023 12:18:44 +0000 (17:48 +0530)
committerrajesh.kumar <rk00747546@techmahindra.com>
Fri, 21 Apr 2023 12:18:44 +0000 (17:48 +0530)
    CPS-1580: Query Across All Anchors Does NOT Filter on Dataspace
    CPS-1582: NullPointerException in queryDataNodesAcrossAnchors

Issue-ID: CPS-1580
Change-ID: I73f97f986a817d423f93a8d922dcd9647b2104ab
Signed-off-by: rajesh.kumar <rk00747546@techmahindra.com>
cps-ri/src/main/java/org/onap/cps/spi/entities/AnchorEntity.java
cps-ri/src/main/java/org/onap/cps/spi/entities/DataspaceEntity.java
cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java
cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy

index b893428..3b0e183 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Pantheon.tech
+ *  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.
@@ -32,6 +33,7 @@ import javax.persistence.Table;
 import javax.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -46,12 +48,14 @@ import lombok.Setter;
 @Builder
 @Entity
 @Table(name = "anchor")
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
 public class AnchorEntity implements Serializable {
 
     private static final long serialVersionUID = -8049987915308262518L;
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @EqualsAndHashCode.Include
     private Integer id;
 
     @NotNull
index 593746d..30906ad 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2020-2021 Nordix Foundation.
  *  Modifications Copyright (C) 2020-2021 Pantheon.tech
+ *  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.
@@ -30,6 +31,7 @@ import javax.persistence.Id;
 import javax.persistence.Table;
 import javax.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -44,12 +46,14 @@ import lombok.Setter;
 @AllArgsConstructor
 @NoArgsConstructor
 @Table(name = "dataspace")
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
 public class DataspaceEntity implements Serializable {
 
     private static final long serialVersionUID = 8395254649813051882L;
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @EqualsAndHashCode.Include
     private Integer id;
 
     @NotNull
index 82afc5a..b90fb79 100755 (executable)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  * Copyright (C) 2020-2023 Nordix Foundation.
  * Modifications Copyright (C) 2021 Pantheon.tech
+ * 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.
@@ -84,10 +85,12 @@ public class FragmentEntity implements Serializable {
     @NotNull
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "dataspace_id")
+    @EqualsAndHashCode.Include
     private DataspaceEntity dataspace;
 
     @OneToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "anchor_id")
+    @EqualsAndHashCode.Include
     private AnchorEntity anchor;
 
     @ToString.Exclude
index 55d3c7e..a33c8d0 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
+ *  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.
@@ -47,6 +48,25 @@ public class FragmentEntityArranger {
         return reuniteChildrenWithTheirParents(fragmentEntityPerId);
     }
 
+    /**
+     * Convert a collection of (related) FragmentExtracts into  FragmentEntities (trees) with descendants.
+     *
+     * @param fragmentExtracts FragmentExtracts to convert.
+     * @param fragmentExtractAnchorMap Map of fragmentExtract with their anchor.
+     * @return a collection of FragmentEntities (trees) with descendants.
+     */
+    public static Collection<FragmentEntity> toFragmentEntityTreesAcrossAnchors(
+            final Collection<FragmentExtract> fragmentExtracts,
+            final Map<Long, AnchorEntity> fragmentExtractAnchorMap) {
+        final Map<Long, FragmentEntity> fragmentEntityPerId = new HashMap<>();
+        for (final FragmentExtract fragmentExtract : fragmentExtracts) {
+            final AnchorEntity anchorEntity = fragmentExtractAnchorMap.get(fragmentExtract.getId());
+            final FragmentEntity fragmentEntity = toFragmentEntity(anchorEntity, fragmentExtract);
+            fragmentEntityPerId.put(fragmentEntity.getId(), fragmentEntity);
+        }
+        return reuniteChildrenWithTheirParents(fragmentEntityPerId);
+    }
+
     private static FragmentEntity toFragmentEntity(final AnchorEntity anchorEntity,
                                                    final FragmentExtract fragmentExtract) {
         final FragmentEntity fragmentEntity = new FragmentEntity();
index aa631d1..0a77de2 100644 (file)
@@ -324,17 +324,20 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         } catch (final PathParsingException e) {
             throw new CpsPathException(e.getMessage());
         }
-
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
         Collection<FragmentEntity> fragmentEntities;
         if (canUseRegexQuickFind(fetchDescendantsOption, cpsPathQuery)) {
-            return getDataNodesUsingRegexQuickFind(fetchDescendantsOption, anchorEntity, cpsPathQuery);
+            return (anchorEntity == ALL_ANCHORS) ? getDataNodesUsingRegexQuickFindAcrossAnchors(fetchDescendantsOption,
+                    dataspaceEntity, cpsPathQuery) : getDataNodesUsingRegexQuickFind(fetchDescendantsOption,
+                    anchorEntity, cpsPathQuery);
         }
-        fragmentEntities = (anchorEntity == ALL_ANCHORS) ? fragmentRepository.findByCpsPath(cpsPathQuery)
+        fragmentEntities = (anchorEntity == ALL_ANCHORS) ? fragmentRepository
+                .findByDataspaceAndCpsPath(dataspaceEntity.getId(), cpsPathQuery)
                 : fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
         if (cpsPathQuery.hasAncestorAxis()) {
             final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
-            fragmentEntities = (anchorEntity == ALL_ANCHORS) ? getAncestorFragmentEntitiesAcrossAnchors(cpsPathQuery,
-            fragmentEntities) : getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption);
+            fragmentEntities = (anchorEntity == ALL_ANCHORS) ? getAncestorFragmentEntitiesAcrossAnchors(dataspaceEntity,
+            cpsPathQuery, fragmentEntities) : getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption);
         }
         return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities);
     }
@@ -357,22 +360,48 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
                                                            final CpsPathQuery cpsPathQuery) {
         Collection<FragmentEntity> fragmentEntities;
         final String xpathRegex = FragmentQueryBuilder.getXpathSqlRegex(cpsPathQuery, true);
-        final List<FragmentExtract> fragmentExtracts = (anchorEntity == ALL_ANCHORS)
-                ? fragmentRepository.quickFindWithDescendantsAcrossAnchor(xpathRegex) :
+        final List<FragmentExtract> fragmentExtracts =
             fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex);
         fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
         if (cpsPathQuery.hasAncestorAxis()) {
             final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
-            fragmentEntities = (anchorEntity == ALL_ANCHORS) ? getAncestorFragmentEntitiesAcrossAnchors(cpsPathQuery,
-            fragmentEntities) : getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption);
+            fragmentEntities = getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption);
         }
         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
-    private Collection<FragmentEntity> getAncestorFragmentEntitiesAcrossAnchors(final CpsPathQuery cpsPathQuery,
-        final Collection<FragmentEntity> fragmentEntities) {
+    private Collection<FragmentEntity> getAncestorFragmentEntitiesAcrossAnchors(final DataspaceEntity dataspaceEntity,
+        final CpsPathQuery cpsPathQuery, final Collection<FragmentEntity> fragmentEntities) {
         final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
-        return ancestorXpaths.isEmpty() ? Collections.emptyList() : fragmentRepository.findAllByXpathIn(ancestorXpaths);
+        return ancestorXpaths.isEmpty() ? Collections.emptyList() : fragmentRepository
+                .findAllByXpathIn(dataspaceEntity, ancestorXpaths);
+    }
+
+    private List<DataNode> getDataNodesUsingRegexQuickFindAcrossAnchors(
+            final FetchDescendantsOption fetchDescendantsOption, final DataspaceEntity dataspaceEntity,
+            final CpsPathQuery cpsPathQuery) {
+        Collection<FragmentEntity> fragmentEntities;
+        final String xpathRegex = FragmentQueryBuilder.getXpathSqlRegex(cpsPathQuery, true);
+        final List<FragmentExtract> fragmentExtracts = fragmentRepository
+                .quickFindWithDescendantsAcrossAnchor(dataspaceEntity.getId(), xpathRegex);
+        final Map<Long, AnchorEntity> fragmentExtractAnchorMap = getFragmentExtractAnchorMap(fragmentExtracts);
+        fragmentEntities = FragmentEntityArranger.toFragmentEntityTreesAcrossAnchors(fragmentExtracts,
+                fragmentExtractAnchorMap);
+        if (cpsPathQuery.hasAncestorAxis()) {
+            fragmentEntities = getAncestorFragmentEntitiesAcrossAnchors(dataspaceEntity,
+                    cpsPathQuery, fragmentEntities);
+        }
+        return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
+    }
+
+    private Map<Long, AnchorEntity> getFragmentExtractAnchorMap(final List<FragmentExtract> fragmentExtracts) {
+        final Map<Long, AnchorEntity> fragmentEntityAnchorMap = new HashMap<>();
+        fragmentExtracts.forEach(fragmentExtract -> {
+            final AnchorEntity anchorEntity = anchorRepository.getById(Math.toIntExact(fragmentExtract.getAnchorId()));
+            fragmentEntityAnchorMap.put(fragmentExtract.getId(), anchorEntity);
+        }
+        );
+        return fragmentEntityAnchorMap;
     }
 
     private List<DataNode> createDataNodesFromProxiedFragmentEntities(
index c231595..5716304 100644 (file)
@@ -75,15 +75,17 @@ public class FragmentQueryBuilder {
     }
 
     /**
-     * Create a sql query to retrieve by cps path.
+     * Create a sql query to retrieve by dataspace id and cps path.
      *
      * @param cpsPathQuery the cps path query to be transformed into a sql query
      * @return a executable query object
      */
-    public Query getQueryForCpsPath(final CpsPathQuery cpsPathQuery) {
-        final StringBuilder sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE xpath ~ :xpathRegex");
+    public Query getQueryForDataspaceAndCpsPath(final int dataspaceId, final CpsPathQuery cpsPathQuery) {
+        final StringBuilder sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE dataspace_id = "
+                + ":dataspaceId AND xpath ~ :xpathRegex");
         final Map<String, Object> queryParameters = new HashMap<>();
         final String xpathRegex = getXpathSqlRegex(cpsPathQuery, false);
+        queryParameters.put("dataspaceId", dataspaceId);
         queryParameters.put("xpathRegex", xpathRegex);
         if (cpsPathQuery.hasLeafConditions()) {
             sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb");
index d486a39..0067cfe 100755 (executable)
@@ -27,6 +27,7 @@ import java.util.Collection;
 import java.util.List;\r
 import java.util.Optional;\r
 import org.onap.cps.spi.entities.AnchorEntity;\r
+import org.onap.cps.spi.entities.DataspaceEntity;\r
 import org.onap.cps.spi.entities.FragmentEntity;\r
 import org.onap.cps.spi.entities.FragmentExtract;\r
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;\r
@@ -54,7 +55,9 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
 \r
     List<FragmentEntity> findAllByAnchorAndXpathIn(AnchorEntity anchorEntity, Collection<String> xpath);\r
 \r
-    List<FragmentEntity> findAllByXpathIn(Collection<String> xpath);\r
+    @Query("SELECT f FROM FragmentEntity f WHERE dataspace = :dataspace AND xpath IN :xpaths")\r
+    List<FragmentEntity> findAllByXpathIn(@Param("dataspace") DataspaceEntity dataspace,\r
+                                          @Param("xpaths") Collection<String> xpaths);\r
 \r
     @Modifying\r
     @Query("DELETE FROM FragmentEntity WHERE anchor IN (:anchors)")\r
@@ -109,7 +112,8 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
 \r
     @Query(value = "SELECT id, anchor_id AS anchorId, xpath, parent_id AS parentId,"\r
             + " CAST(attributes AS TEXT) AS attributes"\r
-            + " FROM FRAGMENT WHERE xpath ~ :xpathRegex",\r
+            + " FROM FRAGMENT WHERE dataspace_id = :dataspaceId AND xpath ~ :xpathRegex",\r
             nativeQuery = true)\r
-    List<FragmentExtract> quickFindWithDescendantsAcrossAnchor(@Param("xpathRegex") String xpathRegex);\r
+    List<FragmentExtract> quickFindWithDescendantsAcrossAnchor(@Param("dataspaceId") int dataspaceId,\r
+                                                               @Param("xpathRegex") String xpathRegex);\r
 }\r
index 32041e7..4195f62 100644 (file)
@@ -28,5 +28,5 @@ import org.onap.cps.spi.entities.FragmentEntity;
 public interface FragmentRepositoryCpsPathQuery {
     List<FragmentEntity> findByAnchorAndCpsPath(int anchorId, CpsPathQuery cpsPathQuery);
 
-    List<FragmentEntity> findByCpsPath(CpsPathQuery cpsPathQuery);
+    List<FragmentEntity> findByDataspaceAndCpsPath(int dataspaceId, CpsPathQuery cpsPathQuery);
 }
index b95491c..3ea5cef 100644 (file)
@@ -51,8 +51,8 @@ public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCps
 
     @Override
     @Transactional
-    public List<FragmentEntity> findByCpsPath(final CpsPathQuery cpsPathQuery) {
-        final Query query = fragmentQueryBuilder.getQueryForCpsPath(cpsPathQuery);
+    public List<FragmentEntity> findByDataspaceAndCpsPath(final int dataspaceId, final CpsPathQuery cpsPathQuery) {
+        final Query query = fragmentQueryBuilder.getQueryForDataspaceAndCpsPath(dataspaceId, cpsPathQuery);
         final List<FragmentEntity> fragmentEntities = query.getResultList();
         log.debug("Fetched {} fragment entities by cps path across all anchors.", fragmentEntities.size());
         return fragmentEntities;
index 60aaa81..fae2c5a 100644 (file)
@@ -193,6 +193,7 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             'String and no descendants'                 | '/shops/shop[@id=1]/categories[@code=1]/book[@title="Dune"]' | OMIT_DESCENDANTS         || 2                            || ['ANCHOR-004', 'ANCHOR-005']
             'Integer and descendants'                   | '/shops/shop[@id=1]/categories[@code=1]/book[@price=5]'      | INCLUDE_ALL_DESCENDANTS  || 3                            || ['ANCHOR-004', 'ANCHOR-005']
             'No condition no descendants'               | '/shops/shop[@id=1]/categories'                              | OMIT_DESCENDANTS         || 6                            || ['ANCHOR-004', 'ANCHOR-005']
+            'top node and all descendants'              | '/shops'                                                     | INCLUDE_ALL_DESCENDANTS  || 2                            || ['ANCHOR-004', 'ANCHOR-005']
             'multiple list-ancestors'                   | '//book/ancestor::categories'                                | INCLUDE_ALL_DESCENDANTS  || 4                            || ['ANCHOR-004', 'ANCHOR-005']
             'one ancestor with list value'              | '//book/ancestor::categories[@code=1]'                       | INCLUDE_ALL_DESCENDANTS  || 2                            || ['ANCHOR-004', 'ANCHOR-005']
             'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]'           | INCLUDE_ALL_DESCENDANTS  || 2                            || ['ANCHOR-004', 'ANCHOR-005']
index f02aa75..fcd8f46 100644 (file)
@@ -212,32 +212,32 @@ class CpsDataPersistenceServiceSpec extends Specification {
         when: 'replace data node tree'
             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
         then: 'call fragment repository save all method'
-            1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities})
+        1 * mockFragmentRepository.saveAll({fragmentEntities -> fragmentEntities.containsAll(expectedFragmentEntities)})
         where: 'the following Data Type is passed'
             scenario                         | dataNodes                                                                          || expectedFragmentEntities
             'empty data node list'           | []                                                                                 || []
-            'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
+            'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [], anchor: new AnchorEntity(id: 123), dataspace: new DataspaceEntity(id: 1))]
     }
 
     def 'update data nodes and descendants'() {
         given: 'the fragment repository returns fragment entities related to the xpath inputs'
-            mockFragmentRepository.findExtractsWithDescendants(123, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
+        mockFragmentRepository.findExtractsWithDescendants(123, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
                 mockFragmentExtract(1, null, 123, '/test/xpath1', null),
                 mockFragmentExtract(2, null, 123, '/test/xpath2', null)
-            ]
+        ]
         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'])])
+        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'])])
         when: 'the fragment entities are update by the data nodes'
-            objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
+        objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
         then: 'call fragment repository save all method is called with the updated fragments'
-            1 * mockFragmentRepository.saveAll({fragmentEntities -> {
-                fragmentEntities.containsAll([
-                    new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
-                    new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
-                ])
-                assert fragmentEntities.size() == 2
-            }})
+        1 * mockFragmentRepository.saveAll({fragmentEntities -> {
+            fragmentEntities.containsAll([
+                    new FragmentEntity(xpath: '/test/xpath1', anchor: new AnchorEntity(id: 123), dataspace: new DataspaceEntity(id: 1), attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
+                    new FragmentEntity(xpath: '/test/xpath2', anchor: new AnchorEntity(id: 123), dataspace: new DataspaceEntity(id: 1), attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
+            ])
+            assert fragmentEntities.size() == 2
+        }})
     }
 
     def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) {
@@ -261,7 +261,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
             dataNodes.add(dataNode)
             def fragmentExtract = mockFragmentExtract(fragmentId, null, null, xpath, null)
             fragmentExtracts.add(fragmentExtract)
-            def fragmentEntity = new FragmentEntity(id: fragmentId, xpath: xpath, childFragments: [])
+            def fragmentEntity = new FragmentEntity(id: fragmentId, xpath: xpath, childFragments: [], anchor: new AnchorEntity(id: 123), dataspace: new DataspaceEntity(id: 1))
             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity
             if ('EXCEPTION' == scenario) {
                 mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }