eclipse-ee4j / eclipselink

Eclipselink project
https://eclipse.dev/eclipselink/
Other
196 stars 167 forks source link

Enum attribute in JPQL is broken when entity identifier is omitted or is 'this.' #2185

Open Riva-Tholoor-Philip opened 2 months ago

Riva-Tholoor-Philip commented 2 months ago

JPQL that omits the optional entity identifier variable or uses this. fails when the entity attribute is an enum type.

For example, from https://github.com/jakartaee/data/blob/77ff9f15920cacd4b51818260ddd4cfe41accebb/tck/src/main/java/ee/jakarta/tck/data/framework/read/only/NaturalNumber.java

which has,

    public enum NumberType {
        ONE, PRIME, COMPOSITE
    }

    private NumberType numType; // enum of ONE | PRIME | COMPOSITE

when the following JPQL is used,

FROM NaturalNumber WHERE isOdd = false AND numType = 
ee.jakarta.tck.data.framework.read.only.NaturalNumber.NumberType.PRIME

or the following JPQL is used (including this.), FROM NaturalNumber WHERE this.isOdd = false AND this.numType = ee.jakarta.tck.data.framework.read.only.NaturalNumber.NumberType.PRIME it fails with this error:

Exception [EclipseLink-6076] (Eclipse Persistence Services - 5.0.0-B02.v202404111748): org.eclipse.persistence.exceptions.QueryException
Exception Description: Object comparisons can only be used with OneToOneMappings. Other mapping comparisons must be done through query keys or direct attribute level comparisons.
Mapping: [org.eclipse.persistence.mappings.DirectToFieldMapping[numType-->WLPNaturalNumber.NUMTYPE]]
Expression: [
Query Key numType
Base ee.jakarta.tck.data.framework.read.only.NaturalNumber]
Query: ReadAllQuery(referenceClass=NaturalNumber jpql=" FROM NaturalNumber WHERE isOdd = false AND numType = ee.jakarta.tck.data.framework.read.only.NaturalNumber.NumberType.PRIME")
at org.eclipse.persistence.exceptions.QueryException.unsupportedMappingForObjectComparison(QueryException.java:1184)
at org.eclipse.persistence.mappings.DatabaseMapping.buildObjectJoinExpression(DatabaseMapping.java:323)
at org.eclipse.persistence.internal.expressions.RelationExpression.normalize(RelationExpression.java:846)
at org.eclipse.persistence.internal.expressions.CompoundExpression.normalize(CompoundExpression.java:257)
at org.eclipse.persistence.internal.expressions.SQLSelectStatement.normalize(SQLSelectStatement.java:1544)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.buildNormalSelectStatement(ExpressionQueryMechanism.java:611)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.prepareSelectAllRows(ExpressionQueryMechanism.java:1783)
at org.eclipse.persistence.queries.ReadAllQuery.prepareSelectAllRows(ReadAllQuery.java:910)
at org.eclipse.persistence.queries.ReadAllQuery.prepare(ReadAllQuery.java:841)
at org.eclipse.persistence.queries.DatabaseQuery.checkPrepare(DatabaseQuery.java:696)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.checkPrepare(ObjectLevelReadQuery.java:1031)
at org.eclipse.persistence.queries.DatabaseQuery.checkPrepare(DatabaseQuery.java:645)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:196)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:120)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:107)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:91)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1846)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1869)
at jdk.proxy11/jdk.proxy11.$Proxy92.two(Unknown Source)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at org.jboss.weld.bean.proxy.AbstractBeanInstance.invoke(AbstractBeanInstance.java:38)
at org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:109)
at ee.jakarta.tck.data.framework.read.only.NaturalNumbers$1173092281$Proxy$_$$_WeldClientProxy.two(Unknown Source)
at ee.jakarta.tck.data.standalone.entity.EntityTest.testLiteralEnumAndLiteralFalse(EntityTest.java:1162)
at ee.jakarta.tck.data.standalone.entity.EntityTest$Proxy$_$$_WeldClientProxy.testLiteralEnumAndLiteralFalse(Unknown Source)
at ee.jakarta.tck.data.standalone.entity.EntityTestServlet.testLiteralEnumAndLiteralFalse(EntityTestServlet.java:234)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at componenttest.app.FATServlet.doGet(FATServlet.java:74)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:633)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:723)
Riva-Tholoor-Philip commented 1 month ago

I could recreate the issue.

Below is the NaturalNumber entity we have by referencing the entity present in Jakarta Data.

@jakarta.persistence.Entity
public class NaturalNumber implements Serializable {
    private static final long serialVersionUID = 1L;

    public enum NumberType {
        ONE, PRIME, COMPOSITE
    }

    @jakarta.persistence.Id
    private long id; //AKA the value
    private boolean isOdd;
    private Short numBitsRequired;
    private NumberType numType; // enum of ONE | PRIME | COMPOSITE
    private long floorOfSquareRoot;

    public static NaturalNumber of(int value) {
        boolean isOne = value == 1;
        boolean isOdd = value % 2 == 1;
        long sqrRoot = squareRoot(value);
        boolean isPrime = isOdd ? isPrime(value, sqrRoot) : (value == 2);

        NaturalNumber inst = new NaturalNumber();
        inst.id = value;
        inst.isOdd = isOdd;
        inst.numBitsRequired = bitsRequired(value);
        inst.numType = isOne ? NumberType.ONE : isPrime ? NumberType.PRIME : NumberType.COMPOSITE;
        inst.floorOfSquareRoot = sqrRoot;

        return inst;
    }

    private static Short bitsRequired(int value) {
        return (short) (Math.floor(Math.log(value) / Math.log(2)) + 1);
    }

    private static long squareRoot(int value) {
        return (long) Math.floor(Math.sqrt(value));
    }

    private static boolean isPrime(int value, long largestPossibleFactor) {
        if (value == 1)
            return false;

        for (int i = 2; i <= largestPossibleFactor; i++) {
            if (value % i == 0)
                return false;
        }
        return true;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public boolean isOdd() {
        return isOdd;
    }

    public void setOdd(boolean isOdd) {
        this.isOdd = isOdd;
    }

    public Short getNumBitsRequired() {
        return numBitsRequired;
    }

    public void setNumBitsRequired(Short numBitsRequired) {
        this.numBitsRequired = numBitsRequired;
    }

    public NumberType getNumType() {
        return numType;
    }

    public void setNumType(NumberType numType) {
        this.numType = numType;
    }

    public long getFloorOfSquareRoot() {
        return floorOfSquareRoot;
    }

    public void setFloorOfSquareRoot(long floorOfSquareRoot) {
        this.floorOfSquareRoot = floorOfSquareRoot;
    }
}

Here is the code where we executed the JPQL query:

public void testOLGH28874() throws Exception {
        NaturalNumber two = NaturalNumber.of(2);
        NaturalNumber three = NaturalNumber.of(3);
        NaturalNumber result1 = null, result2 = null;

        List<Exception> exceptions = new ArrayList<>();

        tx.begin();
        em.persist(two);
        em.persist(three);
        tx.commit();

        tx.begin();
        try {
            result1 = em.createQuery("FROM NaturalNumber WHERE isOdd = false AND numType = io.openliberty.jpa.data.tests.models.NaturalNumber.NumberType.PRIME",
                                     NaturalNumber.class)
                            .getSingleResult();
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            exceptions.add(e);
        }

        tx.begin();
        try {
            result2 = em.createQuery("FROM NaturalNumber WHERE this.isOdd = false AND this.numType = io.openliberty.jpa.data.tests.models.NaturalNumber.NumberType.PRIME",
                                     NaturalNumber.class)
                            .getSingleResult();
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            exceptions.add(e);
        }

        if (!exceptions.isEmpty()) {
            throw exceptions.get(0);
        }

This resulted in the following exception stack, which is the same as described in the issue:

junit.framework.AssertionFailedError: 2024-07-24-17:35:14:205 ERROR: Caught exception attempting to call test method testOLGH28874 on servlet io.openliberty.jpa.data.tests.web.JakartaDataRecreateServlet
Local Exception Stack:
Exception [EclipseLink-6076] (Eclipse Persistence Services - 5.0.0-B02.v202404111748): org.eclipse.persistence.exceptions.QueryException
Exception Description: Object comparisons can only be used with OneToOneMappings. Other mapping comparisons must be done through query keys or direct attribute level comparisons.
Mapping: [org.eclipse.persistence.mappings.DirectToFieldMapping[numType-->NATURALNUMBER.NUMTYPE]]
Expression: [
Query Key numType
Base io.openliberty.jpa.data.tests.models.NaturalNumber]
Query: ReadAllQuery(referenceClass=NaturalNumber jpql="FROM NaturalNumber WHERE isOdd = false AND numType = io.openliberty.jpa.data.tests.models.NaturalNumber.NumberType.PRIME")
at org.eclipse.persistence.exceptions.QueryException.unsupportedMappingForObjectComparison(QueryException.java:1184)
at org.eclipse.persistence.mappings.DatabaseMapping.buildObjectJoinExpression(DatabaseMapping.java:323)
at org.eclipse.persistence.internal.expressions.RelationExpression.normalize(RelationExpression.java:846)
at org.eclipse.persistence.internal.expressions.CompoundExpression.normalize(CompoundExpression.java:257)
at org.eclipse.persistence.internal.expressions.SQLSelectStatement.normalize(SQLSelectStatement.java:1544)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.buildNormalSelectStatement(ExpressionQueryMechanism.java:611)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.prepareSelectAllRows(ExpressionQueryMechanism.java:1783)
at org.eclipse.persistence.queries.ReadAllQuery.prepareSelectAllRows(ReadAllQuery.java:910)
at org.eclipse.persistence.queries.ReadAllQuery.prepare(ReadAllQuery.java:841)
at org.eclipse.persistence.queries.DatabaseQuery.checkPrepare(DatabaseQuery.java:696)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.checkPrepare(ObjectLevelReadQuery.java:1031)
at org.eclipse.persistence.queries.DatabaseQuery.checkPrepare(DatabaseQuery.java:645)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:196)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:120)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:107)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:91)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1846)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1869)

at componenttest.topology.utils.FATServletClient.assertTestResponse(FATServletClient.java:106)
at componenttest.topology.utils.FATServletClient.runTest(FATServletClient.java:91)
at componenttest.custom.junit.runner.SyntheticServletTest.invokeExplosively(SyntheticServletTest.java:49)
at componenttest.custom.junit.runner.FATRunner$1.evaluate(FATRunner.java:204)
at componenttest.custom.junit.runner.FATRunner$2.evaluate(FATRunner.java:364)
at componenttest.custom.junit.runner.FATRunner.run(FATRunner.java:178)
at org.testcontainers.containers.FailureDetectingExternalResource$1.evaluate(FailureDetectingExternalResource.java:29)
at componenttest.rules.repeater.RepeatTests$CompositeRepeatTestActionStatement.evaluate(RepeatTests.java:145)