diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java index d25c865cae9..9652824d095 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java @@ -221,11 +221,22 @@ public static enum Type { final Class returnArrayType; /** - * The type of a single result obtained by the query. - * For example, a single result of a query that returns List is of the type MyEntity. + * The type of a single result obtained by the query. For example, + * A query that returns List has singleType MyEntity. + * A query that returns List> has singleType ArrayList. + * A query that returns Optional has singleType String[]. */ final Class singleType; + /** + * Element type of singleType when singleType is an array or collection. + * Null if singleType is not an array or collection. For example, + * A query that returns List has singleTypeElementType null. + * A query that returns List> has singleTypeElementType String. + * A query that returns Optional has singleTypeElementType String. + */ + final Class singleTypeElementType; + /** * Positions of Sort, Sort[], and Order parameters. * When there are no parameters specifying sort criteria dynamically, @@ -265,25 +276,6 @@ public static enum Type { */ boolean validateResult; - /** - * Constructor for the withJPQL method. - */ - private QueryInfo(Class repositoryInterface, - Method method, - Class entityParamType, - boolean isOptional, - Class multiType, - Class returnArrayType, - Class singleType) { - this.method = method; - this.entityParamType = entityParamType; - this.isOptional = isOptional; - this.multiType = multiType; - this.repositoryInterface = repositoryInterface; - this.returnArrayType = returnArrayType; - this.singleType = singleType; - } - /** * Construct partially complete query information. * @@ -374,11 +366,18 @@ public QueryInfo(Class repositoryInterface, singleType = type; + if ((singleType.isArray() || Iterable.class.isAssignableFrom(singleType)) && + ++d < depth) + singleTypeElementType = returnTypeAtDepth.get(d); + else + singleTypeElementType = null; + if (trace && tc.isEntryEnabled()) Tr.exit(this, tc, "", new Object[] { this, "result isOptional? " + isOptional, "result multiType: " + multiType, - "result singleType: " + singleType }); + "result singleType: " + singleType, + " element: " + singleTypeElementType }); } /** @@ -392,9 +391,43 @@ public QueryInfo(Class repositoryInterface, Method method, Type type) { this.repositoryInterface = repositoryInterface; this.returnArrayType = null; this.singleType = null; + this.singleTypeElementType = null; this.type = type; } + /** + * Construct a copy of a source QueryInfo, but with different JPQL and sorts. + * + * @param source QueryInfo from which to copy. + * @param jpql JPQL to use instead of the JPQL from source. + * @param sorts Sorts to use instead of the sorts from source. + */ + QueryInfo(QueryInfo source, String jpql, List> sorts) { + entityInfo = source.entityInfo; + entityParamType = source.entityParamType; + entityVar = source.entityVar; + entityVar_ = source.entityVar_; + hasWhere = source.hasWhere; + isOptional = source.isOptional; + this.jpql = jpql; + jpqlAfterCursor = source.jpqlAfterCursor; + jpqlBeforeCursor = source.jpqlBeforeCursor; + jpqlCount = source.jpqlCount; + jpqlDelete = source.jpqlDelete; + jpqlParamCount = source.jpqlParamCount; + jpqlParamNames = source.jpqlParamNames; + maxResults = source.maxResults; + method = source.method; + multiType = source.multiType; + repositoryInterface = source.repositoryInterface; + returnArrayType = source.returnArrayType; + singleType = source.singleType; + singleTypeElementType = source.singleTypeElementType; + this.sorts = sorts; + type = source.type; + validateParams = source.validateParams; + } + /** * Adds Sort criteria to the end of the tracked list of sort criteria. * @@ -752,6 +785,11 @@ else if (value instanceof CharSequence) { else if ("false".equalsIgnoreCase(str)) return false; } + } else if (value instanceof List && + Iterable.class.isAssignableFrom(toType)) { + return convertToIterable((List) value, + singleTypeElementType, + toType); } if (failIfNotConverted) { @@ -856,7 +894,7 @@ else if (iterableType.isAssignableFrom(LinkedHashSet.class)) Object[] a = (Object[]) results.get(0); for (int i = 0; i < a.length; i++) { Object element = a[i]; - if (!elementType.isInstance(element)) + if (elementType != null && !elementType.isInstance(element)) element = convert(element, elementType, true); list.add(element); } @@ -5043,35 +5081,4 @@ void validateSort(Sort sort) { repositoryInterface.getName()); } } - - /** - * Copy of query information, but with updated JPQL and sort criteria. - */ - QueryInfo withJPQL(String jpql, List> sorts) { - QueryInfo q = new QueryInfo( // - repositoryInterface, // - method, // - entityParamType, // - isOptional, // - multiType, // - returnArrayType, // - singleType); - q.entityInfo = entityInfo; - q.entityVar = entityVar; - q.entityVar_ = entityVar_; - q.hasWhere = hasWhere; - q.jpql = jpql; - q.jpqlAfterCursor = jpqlAfterCursor; - q.jpqlBeforeCursor = jpqlBeforeCursor; - q.jpqlCount = jpqlCount; - q.jpqlDelete = jpqlDelete; - q.maxResults = maxResults; - q.jpqlParamCount = jpqlParamCount; - q.jpqlParamNames = jpqlParamNames; - q.sorts = sorts; - q.type = type; - q.validateParams = validateParams; - q.validateParams = validateResult; - return q; - } } diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java index 72692d22b28..ea971aeb95d 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java @@ -700,9 +700,10 @@ else if ("toString".equals(methodName)) if (pageReq == null || pageReq.mode() == PageRequest.Mode.OFFSET) { // offset pagination can be a starting point for cursor pagination - queryInfo = queryInfo.withJPQL(q.append(order).toString(), sortList); + String jpql = q.append(order).toString(); + queryInfo = new QueryInfo(queryInfo, jpql, sortList); } else { // CURSOR_NEXT or CURSOR_PREVIOUS - queryInfo = queryInfo.withJPQL(null, sortList); + queryInfo = new QueryInfo(queryInfo, null, sortList); queryInfo.generateCursorQueries(q, forward ? order : null, forward ? null : order); } } @@ -731,7 +732,7 @@ else if ("toString".equals(methodName)) if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) Tr.debug(this, tc, "createQuery", queryInfo.jpql, entityInfo.entityClass.getName()); - TypedQuery query = em.createQuery(queryInfo.jpql, queryInfo.entityInfo.entityClass); + jakarta.persistence.Query query = em.createQuery(queryInfo.jpql); queryInfo.setParameters(query, args); if (queryInfo.type == QueryInfo.Type.FIND_AND_DELETE) @@ -847,7 +848,9 @@ else if (DoubleStream.class.equals(multiType)) returnValue = null; } else if (multiType == null && entityInfo.entityClass.equals(singleType)) { returnValue = oneResult(queryInfo, results); - } else if (multiType != null && multiType.isInstance(results) && (results.isEmpty() || !(results.get(0) instanceof Object[]))) { + } else if (multiType != null && + multiType.isInstance(results) && + (results.isEmpty() || !(results.get(0) instanceof Object[]))) { returnValue = results; } else if (multiType != null && Iterable.class.isAssignableFrom(multiType)) { returnValue = queryInfo.convertToIterable(results, @@ -933,7 +936,14 @@ else if (DoubleStream.class.equals(multiType)) } else if (results.isEmpty()) { throw excEmptyResult(method); } else { // single result of other type - returnValue = oneResult(queryInfo, results); + if (Iterable.class.isAssignableFrom(singleType) && + !(results.get(0) instanceof Iterable)) + // workaround for EclipseLink wrongly returning + // ElementCollection as separate individual elements + // as shown in #30575 + returnValue = results; + else + returnValue = oneResult(queryInfo, results); if (returnValue != null && !singleType.isAssignableFrom(returnValue.getClass())) returnValue = queryInfo.convert(returnValue, diff --git a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java index c1448ad7f3f..323ea31f870 100644 --- a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java +++ b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java @@ -4364,6 +4364,25 @@ public void testQueryByMethodNameWithoutBy() { assertEquals(List.of(), vehicles.findAllOrderByPriceDescVinIdAsc()); } + /** + * Repository Query method that selects and returns a single ArrayList attribute + */ + @Test + public void testQueryReturnsArrayListAttribute() { + assertEquals(new ArrayList(List.of("X", "L", "I")), + primes.romanNumeralSymbolsAsArrayList(41).orElseThrow()); + } + + /** + * Repository Query method that selects a single attribute of type ArrayList + * and returns it as a Collection. + */ + @Test + public void testQueryReturnsArrayListAttributeAsCollection() { + assertEquals(List.of("X", "X", "X", "V", "I", "I"), + primes.romanNumeralSymbolsAsCollection(37).orElseThrow()); + } + /** * Use a Repository method that has the Query annotation and has a return type * that uses a Java record indicating to select a subset of entity attributes. diff --git a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Primes.java b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Primes.java index 4d8f8fbe333..738cf367973 100644 --- a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Primes.java +++ b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Primes.java @@ -427,6 +427,12 @@ Page romanNumeralsWithin(long min1, long max1, long min2, long max2, PageRequest pageRequest); + @Query("SELECT romanNumeralSymbols WHERE numberId = ?1") + Optional> romanNumeralSymbolsAsArrayList(long num); + + @Query("SELECT romanNumeralSymbols WHERE numberId = ?1") + Optional> romanNumeralSymbolsAsCollection(long num); + @Query("SELECT hex WHERE numberId=:id") Optional singleHexDigit(long id); diff --git a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/City.java b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/City.java index 59840610b5a..257b895d595 100644 --- a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/City.java +++ b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/City.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 IBM Corporation and others. + * Copyright (c) 2023,2025 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -23,7 +23,8 @@ @Entity @IdClass(CityId.class) public class City { - // TODO uncomment to reproduce EclipseLink bug with selecting an attribute that is a collection type. + // TODO uncomment to reproduce EclipseLink bugs #28589, #29475 + // that select an attribute that is a collection type. //@ElementCollection(fetch = FetchType.EAGER) public Set areaCodes;