Efficient implementation of Attribute Axis in SQL 99/140599/3
authordanielhanrahan <daniel.hanrahan@est.tech>
Mon, 2 Sep 2024 17:59:41 +0000 (18:59 +0100)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Thu, 27 Mar 2025 10:04:42 +0000 (10:04 +0000)
Attribute Axis is the feature which allows fetching only a single
attribute, e.g. //books[@title='Matilda']/@price -> [15]
This implements the attribute axis feature directly in SQL, giving
much higher performance e.g. for CM-handle ID searches in NCMP.
The native SQL implementation directly returns data leaves from DB,
not requiring conversions to FragmentEntity, DataNode, etc.

Issue-ID: CPS-2623
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: I54f517e47ca6bcddfae356f98857b05fd2e1229e

cps-ri/src/main/java/org/onap/cps/ri/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentQueryBuilder.java
cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepositoryCpsPathQuery.java
cps-ri/src/main/java/org/onap/cps/ri/repository/FragmentRepositoryCpsPathQueryImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/QueryServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy

index 575f9d7..472da34 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 Nordix Foundation
+ *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
@@ -24,7 +24,6 @@
 package org.onap.cps.ri;
 
 import static org.onap.cps.api.CpsQueryService.NO_LIMIT;
-import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS;
 import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION;
 
 import com.google.common.collect.ImmutableSet;
@@ -39,7 +38,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
@@ -249,17 +247,9 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             throw new IllegalArgumentException(
                     "Only Cps Path Queries with attribute-axis are supported by queryDataLeaf");
         }
-
-        final String attributeName = cpsPathQuery.getAttributeAxisAttributeName();
-        final Collection<DataNode> dataNodes = queryDataNodes(dataspaceName, anchorName, cpsPath,
-                OMIT_DESCENDANTS, queryResultLimit);
-        return dataNodes.stream()
-                .map(dataNode -> {
-                    final Object attributeValue = dataNode.getLeaves().get(attributeName);
-                    return targetClass.isInstance(attributeValue) ? targetClass.cast(attributeValue) : null;
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
+        final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
+        return fragmentRepository.findAttributeValuesByAnchorAndCpsPath(anchorEntity, cpsPathQuery,
+                cpsPathQuery.getAttributeAxisAttributeName(), queryResultLimit, targetClass);
     }
 
     @Override
index 0f17b6f..3b88748 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2025 Nordix Foundation
+ *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -61,17 +61,23 @@ public class FragmentQueryBuilder {
      * @return a executable query object
      */
     public Query getQueryForAnchorAndCpsPath(final AnchorEntity anchorEntity,
-                                                      final CpsPathQuery cpsPathQuery,
-                                                      final int queryResultLimit) {
+                                             final CpsPathQuery cpsPathQuery,
+                                             final int queryResultLimit) {
         final StringBuilder sqlStringBuilder = new StringBuilder();
         final Map<String, Object> queryParameters = new HashMap<>();
 
         addSearchPrefix(cpsPathQuery, sqlStringBuilder);
         addWhereClauseForAnchor(anchorEntity, sqlStringBuilder, queryParameters);
+        if (cpsPathQuery.hasAttributeAxis() && !cpsPathQuery.hasAncestorAxis()) {
+            sqlStringBuilder.append(" AND jsonb_exists(fragment.attributes, :attributeName)");
+        }
         addNodeSearchConditions(cpsPathQuery, sqlStringBuilder, queryParameters, false);
         addSearchSuffix(cpsPathQuery, sqlStringBuilder, queryParameters);
         addLimitClause(sqlStringBuilder, queryParameters, queryResultLimit);
-
+        if (cpsPathQuery.hasAttributeAxis()) {
+            queryParameters.put("attributeName", cpsPathQuery.getAttributeAxisAttributeName());
+            return getQuery(sqlStringBuilder.toString(), queryParameters, String.class);
+        }
         return getQuery(sqlStringBuilder.toString(), queryParameters, FragmentEntity.class);
     }
 
@@ -312,7 +318,10 @@ public class FragmentQueryBuilder {
                     WHERE parentFragment.id IN (
                         SELECT parent_id FROM fragment""");
         } else {
-            sqlStringBuilder.append("SELECT fragment.* FROM fragment");
+            final String fieldsToSelect = cpsPathQuery.hasAttributeAxis()
+                    ? "DISTINCT (attributes -> :attributeName)"
+                    : "fragment.*";
+            sqlStringBuilder.append("SELECT ").append(fieldsToSelect).append(" FROM fragment");
         }
     }
 
@@ -327,9 +336,14 @@ public class FragmentQueryBuilder {
                             FROM fragment
                             JOIN ancestors ON ancestors.parent_id = fragment.id
                         )
-                        SELECT * FROM ancestors
-                        WHERE""");
-
+                        """);
+            if (cpsPathQuery.hasAttributeAxis()) {
+                sqlStringBuilder.append("""
+                         SELECT DISTINCT (attributes -> :attributeName) FROM ancestors WHERE
+                         jsonb_exists(ancestors.attributes, :attributeName) AND""");
+            } else {
+                sqlStringBuilder.append("SELECT * FROM ancestors WHERE");
+            }
             final String ancestorPath = DESCENDANT_PATH + cpsPathQuery.getAncestorSchemaNodeIdentifier();
             final CpsPathQuery ancestorCpsPathQuery = CpsPathUtil.getCpsPathQuery(ancestorPath);
             addAncestorNodeSearchCondition(ancestorCpsPathQuery, sqlStringBuilder, queryParameters);
index 50c7494..a24b280 100644 (file)
@@ -1,6 +1,6 @@
 /*-
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 Nordix Foundation.
+ *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,6 +22,7 @@
 package org.onap.cps.ri.repository;
 
 import java.util.List;
+import java.util.Set;
 import org.onap.cps.api.parameters.PaginationOption;
 import org.onap.cps.cpspath.parser.CpsPathQuery;
 import org.onap.cps.ri.models.AnchorEntity;
@@ -33,6 +34,9 @@ public interface FragmentRepositoryCpsPathQuery {
     List<FragmentEntity> findByAnchorAndCpsPath(AnchorEntity anchorEntity, CpsPathQuery cpsPathQuery,
                                                 int queryResultLimit);
 
+    <T> Set<T> findAttributeValuesByAnchorAndCpsPath(AnchorEntity anchorEntity, CpsPathQuery cpsPathQuery,
+                                                     String attributeName, int queryResultLimit, Class<T> targetClass);
+
     List<FragmentEntity> findByDataspaceAndCpsPath(DataspaceEntity dataspaceEntity,
                                                    CpsPathQuery cpsPathQuery, List<Long> anchorIds);
 
index 80fbe9b..cc8055d 100644 (file)
@@ -1,6 +1,6 @@
 /*-
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation.
+ *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,6 +24,8 @@ package org.onap.cps.ri.repository;
 import jakarta.persistence.Query;
 import jakarta.transaction.Transactional;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.parameters.PaginationOption;
@@ -31,20 +33,22 @@ import org.onap.cps.cpspath.parser.CpsPathQuery;
 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.utils.JsonObjectMapper;
 
 @RequiredArgsConstructor
 @Slf4j
 public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery {
 
     private final FragmentQueryBuilder fragmentQueryBuilder;
+    private final JsonObjectMapper jsonObjectMapper;
 
     @Override
     @Transactional
     public List<FragmentEntity> findByAnchorAndCpsPath(final AnchorEntity anchorEntity,
                                                        final CpsPathQuery cpsPathQuery,
                                                        final int queryResultLimit) {
-        final Query query = fragmentQueryBuilder
-                                        .getQueryForAnchorAndCpsPath(anchorEntity, cpsPathQuery, queryResultLimit);
+        final Query query = fragmentQueryBuilder.getQueryForAnchorAndCpsPath(anchorEntity, cpsPathQuery,
+                queryResultLimit);
         final List<FragmentEntity> fragmentEntities = query.getResultList();
         log.debug("Fetched {} fragment entities by anchor and cps path.", fragmentEntities.size());
         if (queryResultLimit > 0) {
@@ -53,6 +57,21 @@ public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCps
         return fragmentEntities;
     }
 
+    @Override
+    @Transactional
+    public <T> Set<T> findAttributeValuesByAnchorAndCpsPath(final AnchorEntity anchorEntity,
+                                                            final CpsPathQuery cpsPathQuery,
+                                                            final String attributeName,
+                                                            final int queryResultLimit,
+                                                            final Class<T> targetClass) {
+        final Query query = fragmentQueryBuilder.getQueryForAnchorAndCpsPath(anchorEntity, cpsPathQuery,
+                queryResultLimit);
+        final List<String> jsonResultList = query.getResultList();
+        return jsonResultList.stream()
+                .map(jsonValue -> jsonObjectMapper.convertJsonString(jsonValue, targetClass))
+                .collect(Collectors.toSet());
+    }
+
     @Override
     @Transactional
     public List<FragmentEntity> findByDataspaceAndCpsPath(final DataspaceEntity dataspaceEntity,
index b319929..138fc34 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2025 Nordix Foundation.
+ *  Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Bell Canada
  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
index aa80e7f..212686e 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 Nordix Foundation
+ *  Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2023-2025 TechMahindra Ltd
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
@@ -66,6 +66,7 @@ class QueryServiceIntegrationSpec extends FunctionalSpecBase {
             'all books'               | '//books/@title'                              || 19
             'all books in a category' | '/bookstore/categories[@code=5]/books/@title' || 10
             'non-existing path'       | '/non-existing/@title'                        || 0
+            'non-existing attribute'  | '//books/@non-existing'                       || 0
     }
 
     def 'Query data leaf with type #leafType using CPS path.'() {
@@ -78,7 +79,7 @@ class QueryServiceIntegrationSpec extends FunctionalSpecBase {
         where:
             leafName    | leafType      || expectedResults
             'lang'      | String.class  || ['English']
-            'price'     | Number.class  || [13, 20]
+            'price'     | Integer.class || [13, 20]
             'editions'  | List.class    || [[1988, 2000], [2006]]
     }
 
@@ -91,6 +92,15 @@ class QueryServiceIntegrationSpec extends FunctionalSpecBase {
             assert result == ['Children', 'Comedy'] as Set
     }
 
+    def 'Attempt to query data leaf without specifying leaf name gives an error.'() {
+        given: 'a cps path without an attribute axis'
+            def cpsPathWithoutAttributeAxis = '//books'
+        when: 'query data leaf is called without attribute axis in cps path'
+            objectUnderTest.queryDataLeaf(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPathWithoutAttributeAxis, String.class)
+        then: 'illegal argument exception is thrown'
+            thrown(IllegalArgumentException)
+    }
+
     def 'Cps Path query using comparative and boolean operators.'() {
         given: 'a cps path query in the discount category'
             def cpsPath = "/bookstore/categories[@code='5']/books" + leafCondition
index 70639c3..8c429b3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 Nordix Foundation
+ *  Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
  *  you may not use this file except in compliance with the License.
@@ -116,9 +116,9 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Query data leaf ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following parameters are used'
             scenario                     | cpsPath                                             || durationLimit  | memoryLimit  | expectedNumberOfValues
-            'unique leaf value'          | '/openroadm-devices/openroadm-device/@device-id'    || 0.10           | 8            | OPENROADM_DEVICES_PER_ANCHOR
-            'common leaf value'          | '/openroadm-devices/openroadm-device/@ne-state'     || 0.05           | 1            | 1
-            'non-existing data leaf'     | '/openroadm-devices/openroadm-device/@non-existing' || 0.05           | 1            | 0
+            'unique leaf value'          | '/openroadm-devices/openroadm-device/@device-id'    || 0.05           | 0.1          | OPENROADM_DEVICES_PER_ANCHOR
+            'common leaf value'          | '/openroadm-devices/openroadm-device/@ne-state'     || 0.02           | 0.1          | 1
+            'non-existing data leaf'     | '/openroadm-devices/openroadm-device/@non-existing' || 0.01           | 0.1          | 0
     }
 
 }