summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordanielhanrahan <daniel.hanrahan@est.tech>2024-09-02 00:16:18 +0100
committerdanielhanrahan <daniel.hanrahan@est.tech>2024-11-26 13:55:10 +0000
commita655c67aa1def9fa5a8924eb6299fb272d6b8f87 (patch)
treef973a662b1cc4e462e4cf9621cfa49808b7c2222
parent05e97441ad192b69051fbb67263284eb99ee1c41 (diff)
Implement ancestor axis in SQL
Currenty ancestor axis is implemented in Java using regex, which then sends a second SQL query to fetch ancestors. Implementing ancestor axis in SQL is more efficient, and is needed for limiting/paginating Cps Path Query results. Issue-ID: CPS-2416 Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech> Change-Id: I0d8933f86c5a422f366ad7c417a17e263a13960f
-rw-r--r--cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java42
-rw-r--r--cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java93
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepository.java16
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy1
4 files changed, 90 insertions, 62 deletions
diff --git a/cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java
index bdbdc7cf36..cacdba93cc 100644
--- a/cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java
@@ -39,8 +39,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -81,8 +79,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
private final JsonObjectMapper jsonObjectMapper;
private final SessionManager sessionManager;
- private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@.+?])?)";
-
@Override
public void storeDataNodes(final String dataspaceName, final String anchorName,
final Collection<DataNode> dataNodes) {
@@ -228,13 +224,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
final FetchDescendantsOption fetchDescendantsOption) {
final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
final CpsPathQuery cpsPathQuery = getCpsPathQuery(cpsPath);
-
- Collection<FragmentEntity> fragmentEntities;
- fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
- if (cpsPathQuery.hasAncestorAxis()) {
- final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
- fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
- }
+ final Collection<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
}
@@ -246,7 +237,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
final PaginationOption paginationOption) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
final CpsPathQuery cpsPathQuery = getCpsPathQuery(cpsPath);
-
final List<Long> anchorIds;
if (paginationOption == NO_PAGINATION) {
anchorIds = Collections.emptyList();
@@ -256,17 +246,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
return Collections.emptyList();
}
}
- Collection<FragmentEntity> fragmentEntities =
- fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
-
- if (cpsPathQuery.hasAncestorAxis()) {
- final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
- if (anchorIds.isEmpty()) {
- fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
- } else {
- fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(anchorIds, ancestorXpaths);
- }
- }
+ final Collection<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
}
@@ -668,21 +649,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
.collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
}
- private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
- final CpsPathQuery cpsPathQuery) {
- final Set<String> ancestorXpath = new HashSet<>();
- final Pattern pattern =
- Pattern.compile("(.*/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
- + REG_EX_FOR_OPTIONAL_LIST_INDEX + "/.*");
- for (final FragmentEntity fragmentEntity : fragmentEntities) {
- final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
- if (matcher.matches()) {
- ancestorXpath.add(matcher.group(1));
- }
- }
- return ancestorXpath;
- }
-
private static boolean isRootXpath(final String xpath) {
return "/".equals(xpath) || "".equals(xpath);
}
diff --git a/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java b/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java
index b8bbf59c23..e35440e29a 100644
--- a/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java
+++ b/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java
@@ -30,8 +30,10 @@ import java.util.List;
import java.util.Map;
import java.util.Queue;
import lombok.RequiredArgsConstructor;
+import org.apache.commons.text.StringSubstitutor;
import org.onap.cps.cpspath.parser.CpsPathPrefixType;
import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
import org.onap.cps.ri.models.AnchorEntity;
import org.onap.cps.ri.models.DataspaceEntity;
import org.onap.cps.ri.models.FragmentEntity;
@@ -43,6 +45,7 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class FragmentQueryBuilder {
+ private static final String DESCENDANT_PATH = "//";
@PersistenceContext
private EntityManager entityManager;
@@ -58,9 +61,10 @@ public class FragmentQueryBuilder {
final StringBuilder sqlStringBuilder = new StringBuilder();
final Map<String, Object> queryParameters = new HashMap<>();
- sqlStringBuilder.append("SELECT fragment.* FROM fragment");
+ addSearchPrefix(cpsPathQuery, sqlStringBuilder);
addWhereClauseForAnchor(anchorEntity, sqlStringBuilder, queryParameters);
addNodeSearchConditions(cpsPathQuery, sqlStringBuilder, queryParameters, false);
+ addSearchSuffix(cpsPathQuery, sqlStringBuilder, queryParameters);
return getQuery(sqlStringBuilder.toString(), queryParameters, FragmentEntity.class);
}
@@ -78,13 +82,14 @@ public class FragmentQueryBuilder {
final StringBuilder sqlStringBuilder = new StringBuilder();
final Map<String, Object> queryParameters = new HashMap<>();
- sqlStringBuilder.append("SELECT fragment.* FROM fragment");
+ addSearchPrefix(cpsPathQuery, sqlStringBuilder);
if (anchorIdsForPagination.isEmpty()) {
addWhereClauseForDataspace(dataspaceEntity, sqlStringBuilder, queryParameters);
} else {
addWhereClauseForAnchorIds(anchorIdsForPagination, sqlStringBuilder, queryParameters);
}
addNodeSearchConditions(cpsPathQuery, sqlStringBuilder, queryParameters, true);
+ addSearchSuffix(cpsPathQuery, sqlStringBuilder, queryParameters);
return getQuery(sqlStringBuilder.toString(), queryParameters, FragmentEntity.class);
}
@@ -143,7 +148,8 @@ public class FragmentQueryBuilder {
final Map<String, Object> queryParameters,
final boolean acrossAnchors) {
addAbsoluteParentXpathSearchCondition(cpsPathQuery, sqlStringBuilder, queryParameters, acrossAnchors);
- addXpathSearchCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
+ sqlStringBuilder.append(" AND ");
+ addXpathSearchCondition(cpsPathQuery, sqlStringBuilder, queryParameters, "baseXpath");
addLeafConditions(cpsPathQuery, sqlStringBuilder);
addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
addContainsFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
@@ -151,13 +157,35 @@ public class FragmentQueryBuilder {
private static void addXpathSearchCondition(final CpsPathQuery cpsPathQuery,
final StringBuilder sqlStringBuilder,
- final Map<String, Object> queryParameters) {
- sqlStringBuilder.append(" AND (xpath LIKE :escapedXpath OR "
- + "(xpath LIKE :escapedXpath||'[@%]' AND xpath NOT LIKE :escapedXpath||'[@%]/%[@%]'))");
+ final Map<String, Object> queryParameters,
+ final String parameterName) {
+ queryParameters.put(parameterName, escapeXpathForSqlLike(cpsPathQuery));
+ final String sqlForXpathLikeContainerOrList = """
+ (
+ (xpath LIKE :${xpathParamName})
+ OR
+ (xpath LIKE :${xpathParamName}||'[@%]' AND xpath NOT LIKE :${xpathParamName}||'[@%]/%[@%]')
+ )
+ """;
+ sqlStringBuilder.append(substitute(sqlForXpathLikeContainerOrList, Map.of("xpathParamName", parameterName)));
+ }
+
+ /**
+ * Returns a pattern suitable for use in an SQL LIKE expression, matching the xpath (absolute or descendant).
+ * For an absolute path such as "/bookstore/categories[@name='10% off']",
+ * the output would be "/bookstore/categories[@name='10\% off']".
+ * For a descendant path such as "//categories[@name='10% off']",
+ * the output would be "%/categories[@name='10\% off']".
+ * Note: percent sign '%' means match anything in SQL LIKE, while underscore '_' means match any single character.
+ *
+ * @param cpsPathQuery Cps Path Query
+ * @return a pattern suitable for use in an SQL LIKE expression.
+ */
+ private static String escapeXpathForSqlLike(final CpsPathQuery cpsPathQuery) {
if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) {
- queryParameters.put("escapedXpath", EscapeUtils.escapeForSqlLike(cpsPathQuery.getXpathPrefix()));
+ return EscapeUtils.escapeForSqlLike(cpsPathQuery.getXpathPrefix());
} else {
- queryParameters.put("escapedXpath", "%/" + EscapeUtils.escapeForSqlLike(cpsPathQuery.getDescendantName()));
+ return "%/" + EscapeUtils.escapeForSqlLike(cpsPathQuery.getDescendantName());
}
}
@@ -261,6 +289,55 @@ public class FragmentQueryBuilder {
}
}
+ private static void addSearchPrefix(final CpsPathQuery cpsPathQuery, final StringBuilder sqlStringBuilder) {
+ if (cpsPathQuery.hasAncestorAxis()) {
+ sqlStringBuilder.append("""
+ WITH RECURSIVE ancestors AS (
+ SELECT parentFragment.* FROM fragment parentFragment
+ WHERE parentFragment.id IN (
+ SELECT parent_id FROM fragment""");
+ } else {
+ sqlStringBuilder.append("SELECT fragment.* FROM fragment");
+ }
+ }
+
+ private static void addSearchSuffix(final CpsPathQuery cpsPathQuery,
+ final StringBuilder sqlStringBuilder,
+ final Map<String, Object> queryParameters) {
+ if (cpsPathQuery.hasAncestorAxis()) {
+ sqlStringBuilder.append("""
+ )
+ UNION
+ SELECT fragment.*
+ FROM fragment
+ JOIN ancestors ON ancestors.parent_id = fragment.id
+ )
+ SELECT * FROM ancestors
+ WHERE""");
+
+ final String ancestorPath = DESCENDANT_PATH + cpsPathQuery.getAncestorSchemaNodeIdentifier();
+ final CpsPathQuery ancestorCpsPathQuery = CpsPathUtil.getCpsPathQuery(ancestorPath);
+ addAncestorNodeSearchCondition(ancestorCpsPathQuery, sqlStringBuilder, queryParameters);
+ }
+ }
+
+ private static void addAncestorNodeSearchCondition(final CpsPathQuery ancestorCpsPathQuery,
+ final StringBuilder sqlStringBuilder,
+ final Map<String, Object> queryParameters) {
+ if (ancestorCpsPathQuery.hasLeafConditions()) {
+ final String pathWithoutSlashes = ancestorCpsPathQuery.getNormalizedXpath().substring(2);
+ queryParameters.put("ancestorXpath", "%/" + EscapeUtils.escapeForSqlLike(pathWithoutSlashes));
+ sqlStringBuilder.append(" xpath LIKE :ancestorXpath");
+ } else {
+ addXpathSearchCondition(ancestorCpsPathQuery, sqlStringBuilder, queryParameters, "ancestorXpath");
+ }
+ }
+
+ private static <V> String substitute(final String template, final Map<String, V> valueMap) {
+ final StringSubstitutor stringSubstitutor = new StringSubstitutor(valueMap);
+ return stringSubstitutor.replace(template);
+ }
+
private static void setQueryParameters(final Query query, final Map<String, Object> queryParameters) {
for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) {
query.setParameter(queryParameter.getKey(), queryParameter.getValue());
diff --git a/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepository.java
index a8c1fd2d4e..d95d322d37 100755
--- a/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepository.java
@@ -26,7 +26,6 @@ package org.onap.cps.ri.repository;
import java.util.Collection;
import java.util.List;
import org.onap.cps.ri.models.AnchorEntity;
-import org.onap.cps.ri.models.DataspaceEntity;
import org.onap.cps.ri.models.FragmentEntity;
import org.onap.cps.ri.utils.EscapeUtils;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -63,21 +62,6 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
return findListByAnchorIdAndEscapedXpath(anchorEntity.getId(), escapedXpath);
}
- @Query(value = "SELECT fragment.* FROM fragment JOIN anchor ON anchor.id = fragment.anchor_id "
- + "WHERE dataspace_id = :dataspaceId AND xpath IN (:xpaths)", nativeQuery = true)
- List<FragmentEntity> findByDataspaceIdAndXpathIn(@Param("dataspaceId") int dataspaceId,
- @Param("xpaths") Collection<String> xpaths);
-
- default List<FragmentEntity> findByDataspaceAndXpathIn(final DataspaceEntity dataspaceEntity,
- final Collection<String> xpaths) {
- return findByDataspaceIdAndXpathIn(dataspaceEntity.getId(), xpaths);
- }
-
- @Query(value = "SELECT * FROM fragment WHERE anchor_id IN (:anchorIds)"
- + " AND xpath IN (:xpaths)", nativeQuery = true)
- List<FragmentEntity> findByAnchorIdsAndXpathIn(@Param("anchorIds") Collection<Long> anchorIds,
- @Param("xpaths") Collection<String> xpaths);
-
@Modifying
@Query(value = "DELETE FROM fragment WHERE anchor_id IN (:anchorIds)", nativeQuery = true)
void deleteByAnchorIdIn(@Param("anchorIds") Collection<Long> anchorIds);
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy
index 5c2a4fc665..3b49cfc415 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy
@@ -224,6 +224,7 @@ class QueryServiceIntegrationSpec extends FunctionalSpecBase {
'ancestor with parent that does not exist' | '//books/ancestor::parentDoesNoExist/categories' || []
'ancestor does not exist' | '//books/ancestor::ancestorDoesNotExist' || []
'ancestor combined with contains condition' | '//books[contains(@title,"Mat")]/ancestor::bookstore' || ["/bookstore"]
+ 'ancestor is the same as search target' | '//books/ancestor::books' || []
}
def 'Query for attribute by cps path of type ancestor with #scenario descendants.'() {