Skip to content

Commit

Permalink
Merge pull request #30532 from njr-11/redeliver-30472-after-fixing-break
Browse files Browse the repository at this point in the history
Redeliver #30472 after fixing break
  • Loading branch information
njr-11 authored Jan 13, 2025
2 parents 28b7d7d + ac0499e commit 5755db4
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
###############################################################################
# Copyright (c) 2022,2024 IBM Corporation and others.
# Copyright (c) 2022,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
Expand Down Expand Up @@ -1000,3 +1000,20 @@ CWWKD1100.cursor.requires.sort.explanation=Cursor-based pagination requires \
CWWKD1100.cursor.requires.sort.useraction=Remove the ORDER BY clause from the \
Query annotation value if one is found there. Add an OrderBy annotation or a \
repository method parameter to specify the sort criteria.

CWWKD1101.attr.subset.mismatch=CWWKD1101E: The {0} return type of the \
{1} method of the {2} repository uses the {3} Java record type to define the \
results to be a subset of entity attributes: {4}. However, at least one of the \
requested attribute names is not present on the {5} entity. To return a subset \
of entity attributes as a Java record, ensure that the record component names \
are within the following set of valid attribute names for the {5} entity: {6}. \
Alternatively, use the Select annotation to assign valid entity attribute names. \
To return a single entity attribute that has a Java record type, annotate \
the {1} method to have a Select annotation and assign its member value \
to the name of the entity attribute.
CWWKD1101.attr.subset.mismatch.explanation=When Java records are used to \
retrieve a subset of entity attributes, all of the record components must \
correspond to valid entity attributes.
CWWKD1101.attr.subset.mismatch.useraction=Rename the record components to \
match valid entity attribute names, or use the Select annotation to explicitly \
request entity attributes by their name.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022,2024 IBM Corporation and others.
* Copyright (c) 2022,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
Expand Down Expand Up @@ -124,6 +124,7 @@ public class EntityInfo {
validate();
}

@Trivial
Collection<String> getAttributeNames() {
return attributeNames.values();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022,2024 IBM Corporation and others.
* Copyright (c) 2022,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
Expand Down Expand Up @@ -2135,6 +2135,7 @@ private StringBuilder generateQueryByParameters(StringBuilder q,
*
* @return the SELECT clause.
*/
@FFDCIgnore(RuntimeException.class) // caught to switch to better error
private StringBuilder generateSelectClause() {
StringBuilder q = new StringBuilder(200);
String o = entityVar;
Expand Down Expand Up @@ -2184,65 +2185,91 @@ private StringBuilder generateSelectClause() {
// Whole entity
if (!"this".equals(o))
q.append("SELECT ").append(o);
} else {
// Look for single entity attribute with the desired type:
String singleAttributeName = null;
for (Map.Entry<String, Class<?>> entry : entityInfo.attributeTypes.entrySet()) {
Class<?> attributeType = entry.getValue();
if (attributeType.isPrimitive())
attributeType = Util.wrapperClassIfPrimitive(attributeType);
if (singleType.isAssignableFrom(attributeType))
if (singleAttributeName == null)
singleAttributeName = entry.getKey();
else
throw exc(MappingException.class,
"CWWKD1008.ambig.rtrn.err",
method.getGenericReturnType().getTypeName(),
method.getName(),
repositoryInterface.getName(),
List.of(singleAttributeName, entry.getKey()));
} else if (entityInfo.idClassAttributeAccessors != null &&
singleType.equals(entityInfo.idType)) {
// IdClass
// TODO remove once #29073 is fixed
// The following guess of alphabetic order is not valid in most cases, but this
// whole code block will be removed before GA, so there is no reason to correct it.
q.append("SELECT NEW ").append(singleType.getName()).append('(');
boolean first = true;
for (String idClassAttributeName : entityInfo.idClassAttributeAccessors.keySet()) {
String name = getAttributeName(idClassAttributeName, true);
q.append(first ? "" : ", ").append(o_).append(name);
first = false;
}
q.append(')');
// TODO enable this once #29073 is fixed
// q.append("SELECT ID(").append(entityVar).append(')');
} else {
// Is the result type a record or a single attribute?
RecordComponent[] recordComponents = singleType.getRecordComponents();
if (recordComponents == null) {
// Look for single entity attribute with the desired type:
String singleAttributeName = null;
for (Map.Entry<String, Class<?>> entry : entityInfo.attributeTypes.entrySet()) {
Class<?> attributeType = entry.getValue();
if (attributeType.isPrimitive())
attributeType = Util.wrapperClassIfPrimitive(attributeType);
if (singleType.isAssignableFrom(attributeType))
if (singleAttributeName == null)
singleAttributeName = entry.getKey();
else
throw exc(MappingException.class,
"CWWKD1008.ambig.rtrn.err",
method.getGenericReturnType().getTypeName(),
method.getName(),
repositoryInterface.getName(),
List.of(singleAttributeName, entry.getKey()));
}

if (singleAttributeName == null) {
// TODO enable this once #29073 is fixed
//if (entityInfo.idClassAttributeAccessors != null && singleType.equals(entityInfo.idType)) {
// // IdClass
// q.append("SELECT ID(").append(entityVar).append(')');
// } else
{
// Construct new instance for record
q.append("SELECT NEW ").append(singleType.getName()).append('(');
RecordComponent[] recordComponents;
if (singleAttributeName == null)
throw exc(MappingException.class,
"CWWKD1005.find.rtrn.err",
method.getName(),
repositoryInterface.getName(),
method.getGenericReturnType().getTypeName(),
entityInfo.entityClass.getName(),
List.of("List", "Optional",
"Page", "CursoredPage",
"Stream"));

else
q.append("SELECT ").append(o_).append(singleAttributeName);
} else {
// Construct new instance for record
q.append("SELECT NEW ").append(singleType.getName()).append('(');

String[] names = new String[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
// 1.1 TODO first check for Select annotation on record component
names[i] = recordComponents[i].getName();
}

try {
boolean first = true;
if ((recordComponents = singleType.getRecordComponents()) != null)
for (RecordComponent component : recordComponents) {
String name = component.getName();
q.append(first ? "" : ", ").append(o_).append(name);
first = false;
}
// TODO remove else block once #29073 is fixed
else if (entityInfo.idClassAttributeAccessors != null && singleType.equals(entityInfo.idType))
// The following guess of alphabetic order is not valid in most cases, but the
// whole code block that will be removed before GA, so there is no reason to correct it.
for (String idClassAttributeName : entityInfo.idClassAttributeAccessors.keySet()) {
String name = getAttributeName(idClassAttributeName, true);
q.append(first ? "" : ", ").append(o_).append(name);
first = false;
}
else
throw exc(MappingException.class,
"CWWKD1005.find.rtrn.err",
method.getName(),
repositoryInterface.getName(),
method.getGenericReturnType().getTypeName(),
entityInfo.entityClass.getName(),
List.of("List", "Optional",
"Page", "CursoredPage",
"Stream"));
q.append(')');
for (String name : names) {
name = getAttributeName(name, true);
q.append(first ? "" : ", ");
appendAttributeName(name, q);
first = false;
}
} catch (RuntimeException x) {
// Raise a more precise error that relates to using records
// for a subset of entity attributes
MappingException mx;
mx = exc(MappingException.class,
"CWWKD1101.attr.subset.mismatch",
method.getGenericReturnType().getTypeName(),
method.getName(),
repositoryInterface.getName(),
singleType.getName(),
Arrays.toString(names),
entityInfo.getType().getName(),
entityInfo.getAttributeNames());
throw (MappingException) mx.initCause(x);
}
} else {
q.append("SELECT ").append(o_).append(singleAttributeName);
q.append(')');
}
}
} else { // Individual columns are requested by @Select
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
*******************************************************************************/
package test.jakarta.data.web;

import java.util.stream.Stream;

import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.OrderBy;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;

import test.jakarta.data.web.Animal.ScientificName;
Expand All @@ -26,4 +30,10 @@ public interface Animals extends CrudRepository<Animal, ScientificName> {
long countByIdNotNull();

boolean existsById(ScientificName id);

// Using @Find here would require @Select(ID),
// which is not available in Data 1.0 // TODO move to 1.1 tests
@Query("SELECT id WHERE id.genus = ?1")
@OrderBy("id.species")
Stream<ScientificName> ofGenus(String id_genus);
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ public class DataTestServlet extends FATServlet {
@Inject
Products products;

@Inject
Purchases purchases;

@Inject
Ratings ratings;

Expand Down Expand Up @@ -2045,6 +2048,12 @@ public void testFindByEmbeddedId() {
assertEquals(1, foxSquirrel.version());

// TODO enable once #29460 is fixed
//assertEquals(List.of("Sciurus carolinensis",
// "Sciurus niger"),
// animals.ofGenus("Sciurus")
// .map(n -> n.genus() + ' ' + n.species())
// .collect(Collectors.toList()));

//ScientificName grayFoxId = new ScientificName("Urocyon", "cinereoargenteus");
//grayFox = animals.findById(grayFoxId).orElseThrow();
//assertEquals("gray fox", grayFox.commonName());
Expand Down Expand Up @@ -4565,6 +4574,10 @@ public void testRecordAsEmbeddable() {

assertEquals("Simon", participants.getFirstName(3).orElseThrow());

// TODO enable once #29460 is fixed
//assertEquals(new Participant.Name("Samantha", "TestRecordAsEmbeddable"),
// participants.findNameById(4).orElseThrow());

// TODO enable once #29460 is fixed
//assertEquals(List.of("Samantha", "Sarah", "Simon", "Steve"),
// participants.withSurname("TestRecordAsEmbeddable")
Expand Down Expand Up @@ -4594,6 +4607,31 @@ public void testRecordInFromClause() {
assertEquals(2, receipts.removeIfTotalUnder(2000.0f));
}

/**
* Verify that a record return type (per spec) takes precedence over an
* entity attribute that is a record.
*/
@Test
public void testRecordReturnTypePrecedence() {
purchases.clearAll();

Purchase p1 = new Purchase();
p1.total = 105.19f;
p1.customer = "TestRecordReturnTypePrecedence";
p1.purchaseId = 1L;
// the following does not match on purpose
p1.receipt = new Receipt(1001L, "Customer1", 1.99f);

purchases.buy(p1);

Receipt r1 = purchases.receiptFor(1L).orElseThrow();
assertEquals("TestRecordReturnTypePrecedence", r1.customer());
assertEquals(1L, r1.purchaseId());
assertEquals(105.19f, r1.total(), 0.001f);

purchases.clearAll();
}

/**
* Use repository methods that have various return types for a record entity.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;

import test.jakarta.data.web.Participant.Name;

/**
* Repository for an unannotated entity with a record attribute
* that should be interpreted as an embeddable.
Expand All @@ -34,6 +36,11 @@ public interface Participants extends DataRepository<Participant, Integer> {
@Insert
void add(Participant... p);

// Using Query by Method Name would require @Select("name"),
// which is not available in Data 1.0 // TODO move to 1.1 tests
@Query("SELECT name WHERE id = ?1")
Optional<Name> findNameById(int id);

@Query("SELECT name.first WHERE id = ?1")
Optional<String> getFirstName(int id);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 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
* http://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package test.jakarta.data.web;

/**
* Entity where one of the attributes is a record that is in this case used as
* an embeddable, but elsewhere used as an entity.
*/
public class Purchase {
public String customer;

public long purchaseId;

public Receipt receipt;

public float total;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*******************************************************************************
* Copyright (c) 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
* http://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package test.jakarta.data.web;

import java.util.Optional;

import jakarta.data.repository.DataRepository;
import jakarta.data.repository.Delete;
import jakarta.data.repository.Find;
import jakarta.data.repository.Insert;
import jakarta.data.repository.Repository;

/**
* Repository for an entity where one of the attributes is a record that is in
* this case used as an embeddable, but elsewhere used as an entity.
*/
@Repository
public interface Purchases extends DataRepository<Purchase, Long> {

@Insert
void buy(Purchase purchase);

@Delete
void clearAll();

// Selects multiple entity attributes into a record, per the spec
@Find
Optional<Receipt> receiptFor(long purchaseId);

}
Loading

0 comments on commit 5755db4

Please sign in to comment.