This is an automated email from the ASF dual-hosted git repository.

pcristof pushed a commit to branch OPENJPA-2940
in repository https://gitbox.apache.org/repos/asf/openjpa.git


The following commit(s) were added to refs/heads/OPENJPA-2940 by this push:
     new 4ad9ceb40 [WIP][OPENJPA-2940] Implementing JPQL CAST TO STRING
4ad9ceb40 is described below

commit 4ad9ceb400a1dfc6ae1fcc5d9ad805197ededab4
Author: Paulo Cristovão de Araújo Silva Filho <[email protected]>
AuthorDate: Thu Sep 11 18:27:17 2025 -0300

    [WIP][OPENJPA-2940] Implementing JPQL CAST TO STRING
---
 .../jdbc/kernel/exps/JDBCExpressionFactory.java    |   5 +
 .../openjpa/jdbc/kernel/exps/TypecastAsString.java | 175 +++++++++++++++++++++
 .../org/apache/openjpa/jdbc/sql/DBDictionary.java  |   2 +-
 .../apache/openjpa/jdbc/sql/DerbyDictionary.java   |   2 +-
 .../openjpa/kernel/exps/ExpressionFactory.java     |   7 +-
 .../kernel/exps/InMemoryExpressionFactory.java     |   5 +
 .../openjpa/kernel/exps/TypecastAsString.java      |  96 +++++++++++
 .../openjpa/kernel/jpql/JPQLExpressionBuilder.java |   3 +
 .../jjtree/org/apache/openjpa/kernel/jpql/JPQL.jjt |  13 +-
 .../apache/openjpa/kernel/jpql/TestJPQLParser.java |  23 +++
 .../jpql/functions/TestEJBQLFunction.java          |  31 ++++
 11 files changed, 356 insertions(+), 6 deletions(-)

diff --git 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/JDBCExpressionFactory.java
 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/JDBCExpressionFactory.java
index e0f7d67d4..1a7cf122f 100644
--- 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/JDBCExpressionFactory.java
+++ 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/JDBCExpressionFactory.java
@@ -380,6 +380,11 @@ public class JDBCExpressionFactory
     public Value getDateTimePart(DateTimeExtractPart part, Value value) {
         return new ExtractDateTimePart((Val) value, part);
     }
+    
+    @Override
+    public Value newTypecastAsString(Value value) {
+       return new TypecastAsString((Val) value);
+    }
 
     @Override
     public Parameter newParameter(Object name, Class type) {
diff --git 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsString.java
 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsString.java
new file mode 100644
index 000000000..39cdd1e74
--- /dev/null
+++ 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsString.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel.exps;
+
+import java.sql.SQLException;
+
+import org.apache.openjpa.jdbc.meta.JavaSQLTypes;
+import org.apache.openjpa.jdbc.sql.DBDictionary;
+import org.apache.openjpa.jdbc.sql.Joins;
+import org.apache.openjpa.jdbc.sql.Result;
+import org.apache.openjpa.jdbc.sql.SQLBuffer;
+import org.apache.openjpa.jdbc.sql.Select;
+import org.apache.openjpa.kernel.Filters;
+import org.apache.openjpa.kernel.exps.ExpressionVisitor;
+import org.apache.openjpa.meta.ClassMetaData;
+
+/**
+ * Returns the temporal field of a given date or time.
+ *
+ */
+public class TypecastAsString
+    extends AbstractVal {
+
+    private static final long serialVersionUID = 1L;
+    private final Val _val;
+    private ClassMetaData _meta = null;
+
+    /**
+     * Constructor. Provides the value to be casted to string.
+     */
+    public TypecastAsString(Val val) {
+        _val = val;
+    }
+
+    public Val getVal() {
+        return _val;
+    }
+
+    @Override
+    public ClassMetaData getMetaData() {
+        return _meta;
+    }
+
+    @Override
+    public void setMetaData(ClassMetaData meta) {
+        _meta = meta;
+    }
+
+    @Override
+    public Class getType() {
+        return String.class;
+    }
+
+    @Override
+    public void setImplicitType(Class type) {
+    }
+
+    @Override
+    public ExpState initialize(Select sel, ExpContext ctx, int flags) {
+        ExpState valueState =  _val.initialize(sel, ctx, 0);
+        return new TypecastAsStringExpState(valueState.joins, valueState);
+    }
+
+    /**
+     * Expression state.
+     */
+    private static class TypecastAsStringExpState
+        extends ExpState {
+
+        public final ExpState valueState;
+
+        public TypecastAsStringExpState(Joins joins, ExpState valueState) {
+            super(joins);
+            this.valueState = valueState;
+        }
+    }
+
+    @Override
+    public void select(Select sel, ExpContext ctx, ExpState state,
+        boolean pks) {
+        sel.select(newSQLBuffer(sel, ctx, state), this);
+    }
+
+    @Override
+    public void selectColumns(Select sel, ExpContext ctx, ExpState state, 
boolean pks) {
+       TypecastAsStringExpState casstate = (TypecastAsStringExpState) state;
+        _val.selectColumns(sel, ctx, casstate.valueState, true);
+    }
+
+    @Override
+    public void groupBy(Select sel, ExpContext ctx, ExpState state) {
+        sel.groupBy(newSQLBuffer(sel, ctx, state));
+    }
+
+    @Override
+    public void orderBy(Select sel, ExpContext ctx, ExpState state,
+        boolean asc) {
+        sel.orderBy(newSQLBuffer(sel, ctx, state), asc, false, getSelectAs());
+    }
+
+    private SQLBuffer newSQLBuffer(Select sel, ExpContext ctx, ExpState state) 
{
+        calculateValue(sel, ctx, state, null, null);
+        SQLBuffer buf = new SQLBuffer(ctx.store.getDBDictionary());
+        appendTo(sel, ctx, state, buf, 0);
+        return buf;
+    }
+
+    @Override
+    public Object load(ExpContext ctx, ExpState state, Result res)
+        throws SQLException {
+        return Filters.convert(res.getObject(this,
+            JavaSQLTypes.JDBC_DEFAULT, null), getType());
+    }
+
+    @Override
+    public void calculateValue(Select sel, ExpContext ctx, ExpState state,
+        Val other, ExpState otherState) {
+       TypecastAsStringExpState casstate = (TypecastAsStringExpState) state;
+        _val.calculateValue(sel, ctx, casstate.valueState, null, null);
+    }
+
+    @Override
+    public int length(Select sel, ExpContext ctx, ExpState state) {
+        return 1;
+    }
+
+    @Override
+    public void appendTo(Select sel, ExpContext ctx, ExpState state,
+        SQLBuffer sql, int index) {
+        DBDictionary dict = ctx.store.getDBDictionary();
+        String func = dict.castFunction;
+
+        int fieldPart = func.indexOf("{0}");
+        int targetPart = func.indexOf("{1}");
+        String part1 = func.substring(0, fieldPart);
+        String part2 = func.substring(fieldPart + 3, targetPart);
+        String part3 = func.substring(targetPart + 3);
+
+        TypecastAsStringExpState casstate = (TypecastAsStringExpState) state;
+        sql.append(part1);
+        _val.appendTo(sel, ctx, casstate.valueState, sql, 0);
+        sql.append(part2);
+       sql.append(dict.varcharTypeName);
+        sql.append(part3);
+    }
+
+    @Override
+    public void acceptVisit(ExpressionVisitor visitor) {
+        visitor.enter(this);
+        _val.acceptVisit(visitor);
+        visitor.exit(this);
+    }
+
+    @Override
+    public int getId() {
+        return Val.EXTRACTDTF_VAL;
+    }
+}
+
diff --git 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
index fdfd15afa..7f0346a39 100644
--- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
+++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
@@ -315,7 +315,7 @@ public class DBDictionary
     public int maxEmbeddedBlobSize = -1;
     public int maxEmbeddedClobSize = -1;
     public int inClauseLimit = -1;
-
+    
     /**
      * Attention, while this is named datePrecision it actually only get used 
for Timestamp handling!
      * @see StateManagerImpl#roundTimestamp(Timestamp, int)
diff --git 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java
index 8fcbb8eb9..804a4f214 100644
--- 
a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java
+++ 
b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java
@@ -69,7 +69,7 @@ public class DerbyDictionary
         supportsNullUniqueColumn = false;
 
         supportsComments = true;
-
+        
         // Derby does still not support 'WITH TIMEZONE' from the SQL92 standard
 
         fixedSizeTypeNameSet.addAll(Arrays.asList(new String[]{
diff --git 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/ExpressionFactory.java
 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/ExpressionFactory.java
index ded5ab1cf..8a517a155 100644
--- 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/ExpressionFactory.java
+++ 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/ExpressionFactory.java
@@ -18,7 +18,6 @@
  */
 package org.apache.openjpa.kernel.exps;
 
-import java.time.temporal.ChronoField;
 import java.time.temporal.Temporal;
 import java.util.Date;
 
@@ -258,6 +257,12 @@ public interface ExpressionFactory {
      * Return the Date or time part of the given temporal value
      */
     Value getDateTimePart(DateTimeExtractPart part, Value value);
+    
+    /**
+     * Returns the value typecasted as string
+     * 
+     */
+    Value newTypecastAsString(Value value);
 
     /**
      * Return a value representing a parameter for the given value. The
diff --git 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/InMemoryExpressionFactory.java
 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/InMemoryExpressionFactory.java
index e58a737f6..bc936b9cc 100644
--- 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/InMemoryExpressionFactory.java
+++ 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/InMemoryExpressionFactory.java
@@ -554,6 +554,11 @@ public class InMemoryExpressionFactory
     public Value getDateTimePart(DateTimeExtractPart part, Value value) {
         return new ExtractDateTimePart(part, (Val) value);
     }
+    
+    @Override
+    public Value newTypecastAsString(Value value) {
+       return new TypecastAsString((Val) value);
+    }
 
     @Override
     public Parameter newParameter(Object name, Class type) {
diff --git 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/TypecastAsString.java
 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/TypecastAsString.java
new file mode 100644
index 000000000..0bed9230c
--- /dev/null
+++ 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/TypecastAsString.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.kernel.exps;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+import org.apache.openjpa.kernel.StoreContext;
+
+/**
+ * Casts a given value as string
+ *
+ */
+class TypecastAsString
+    extends Val {
+
+    
+    private static final long serialVersionUID = 1L;
+    private final Val _val;
+
+    /**
+     * Constructor. Provide target field and the value.
+     */
+    public TypecastAsString(Val val) {
+        _val = val;
+    }
+
+    @Override
+    public Class getType() {
+       return String.class;
+    }
+
+    @Override
+    public void setImplicitType(Class type) {
+    }
+
+    @Override
+    protected Object eval(Object candidate, Object orig,
+        StoreContext ctx, Object[] params) {
+
+        Object r = _val.eval(candidate, orig, ctx, params);
+        Class<?> clazz = r.getClass();
+        if (Time.class.isAssignableFrom(clazz)) {
+            return ((Time) r).toLocalTime().format(DateTimeFormatter.ISO_TIME);
+        } else if (Timestamp.class.isAssignableFrom(clazz)) {
+            return ((Timestamp) 
r).toLocalDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        } else if (LocalDateTime.class.isAssignableFrom(clazz)) {
+            return ((LocalDateTime) 
r).toLocalTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        } else if (LocalTime.class.isAssignableFrom(clazz)) {
+            return ((LocalTime) r).format(DateTimeFormatter.ISO_LOCAL_TIME);
+        } else if (Instant.class.isAssignableFrom(clazz)) {
+            return LocalDateTime.ofInstant((Instant) r, 
ZoneId.systemDefault()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        } else if (Date.class.isAssignableFrom(clazz)) {
+            return LocalDateTime.ofInstant(((Date) r).toInstant(), 
ZoneId.systemDefault()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        } else if (LocalDate.class.isAssignableFrom(clazz)) {
+            return ((LocalDate) r).format(DateTimeFormatter.ISO_LOCAL_DATE);
+        } else if (Number.class.isAssignableFrom(clazz)) {
+               return String.valueOf((Number) r);
+        } else if (Boolean.class.isAssignableFrom(clazz)) {
+               return String.valueOf((Boolean) r);
+        }
+        throw new IllegalArgumentException();
+    }
+
+    @Override
+    public void acceptVisit(ExpressionVisitor visitor) {
+        visitor.enter(this);
+        _val.acceptVisit(visitor);
+        visitor.exit(this);
+    }
+
+}
+
diff --git 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/jpql/JPQLExpressionBuilder.java
 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/jpql/JPQLExpressionBuilder.java
index 8d262f1bc..5b3f31864 100644
--- 
a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/jpql/JPQLExpressionBuilder.java
+++ 
b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/jpql/JPQLExpressionBuilder.java
@@ -1521,6 +1521,9 @@ public class JPQLExpressionBuilder
 
             case JJTTIMESTAMPLITERAL:
                 return factory.newLiteral(node.text, Literal.TYPE_TIMESTAMP);
+                
+            case JJTSTRINGCAST:
+               return factory.newTypecastAsString(getValue(onlyChild(node)));
 
             default:
                 throw parseException(EX_FATAL, "bad-tree",
diff --git 
a/openjpa-kernel/src/main/jjtree/org/apache/openjpa/kernel/jpql/JPQL.jjt 
b/openjpa-kernel/src/main/jjtree/org/apache/openjpa/kernel/jpql/JPQL.jjt
index a746ceffe..5bcf0ccac 100644
--- a/openjpa-kernel/src/main/jjtree/org/apache/openjpa/kernel/jpql/JPQL.jjt
+++ b/openjpa-kernel/src/main/jjtree/org/apache/openjpa/kernel/jpql/JPQL.jjt
@@ -170,6 +170,8 @@ TOKEN [ IGNORE_CASE ]: /* basics */
        |       < ENTRY: "ENTRY" >
        |   < LOCAL: "LOCAL" >
        |   < EXTRACT: "EXTRACT" >
+       |   < CAST: "CAST" >
+       |   < STRING: "STRING" >
 }
 
 TOKEN [ IGNORE_CASE ]: /* aggregates */
@@ -1122,7 +1124,7 @@ void string_value() : { }
 
 void string_expression() : { }
 {
-       input_parameter() | string_primary()
+       input_parameter() | string_primary() 
 }
 
 
@@ -1133,8 +1135,9 @@ void string_primary() : { }
        LOOKAHEAD(general_identification_variable()) 
general_identification_variable() |
        LOOKAHEAD(identification_variable()) identification_variable() |
        LOOKAHEAD("(" string_expression()) "(" string_expression() ")" |
-       functions_returning_strings() | LOOKAHEAD("(" subquery()) "(" 
subquery() ")"
-       | case_expression()
+       functions_returning_strings() | 
+       string_cast_function() |
+       LOOKAHEAD("(" subquery()) "(" subquery() ")" | case_expression()
 }
 
 
@@ -1222,6 +1225,10 @@ void functions_returning_strings() : { }
        concat() | substring() | trim() | lower() | upper()
 }
 
+void string_cast_function() #STRINGCAST : { }
+{
+       <CAST> "(" scalar_expression() <AS> <STRING> ")"
+}
 
 void concat() #CONCAT : { }
 {
diff --git 
a/openjpa-kernel/src/test/java/org/apache/openjpa/kernel/jpql/TestJPQLParser.java
 
b/openjpa-kernel/src/test/java/org/apache/openjpa/kernel/jpql/TestJPQLParser.java
index 3b9fb2109..facfb9b3e 100644
--- 
a/openjpa-kernel/src/test/java/org/apache/openjpa/kernel/jpql/TestJPQLParser.java
+++ 
b/openjpa-kernel/src/test/java/org/apache/openjpa/kernel/jpql/TestJPQLParser.java
@@ -70,5 +70,28 @@ public class TestJPQLParser {
             fail();
         }
     }
+    
+    @Test
+    public void testSimpleCastToString() {
+       try {
+               String query = "SELECT u FROM User AS u WHERE 
CAST((EXTRACT(year FROM u.dateOfBirth)) AS STRING) = '1983'";
+               JPQLNode node = (JPQLNode) new JPQL(query).parseQuery();
+               assertNotNull(node);
+       } catch (ParseException ex) {
+               ex.printStackTrace();
+               fail();
+       }
+    }
 
+    @Test
+    public void testSimpleCastToStringOnSelect() {
+       try {
+               String query = "SELECT CAST(u.birthYear AS string) FROM User AS 
u WHERE EXTRACT(year FROM u.dateOfBirth) = 1983";
+               JPQLNode node = (JPQLNode) new JPQL(query).parseQuery();
+               assertNotNull(node);
+       } catch (ParseException ex) {
+               ex.printStackTrace();
+               fail();
+       }
+    }
 }
diff --git 
a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java
 
b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java
index 549e947b5..432ed6723 100644
--- 
a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java
+++ 
b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java
@@ -863,6 +863,37 @@ public class TestEJBQLFunction extends AbstractTestCase {
 
         endEm(em);
     }
+    
+    public void testTypecastAsString() {
+       if (getDbDictionary(getEmf()) instanceof DerbyDictionary) {
+               // Derby does not support CAST from integer to VARCHAR
+               return;
+       }
+       EntityManager em = currentEntityManager();
+       String query = "SELECT u FROM CompUser AS u WHERE CAST(u.age AS STRING) 
= '23'";
+       
+       List result = em.createQuery(query).getResultList();
+       
+       assertEquals(1, result.size());
+       
+       endEm(em);
+    }
+
+    public void testTypecastAsStringOnSelect() {
+       if (getDbDictionary(getEmf()) instanceof DerbyDictionary) {
+               // Derby does not support CAST from integer to VARCHAR
+               return;
+       }
+       EntityManager em = currentEntityManager();
+       String query = "SELECT CAST(u.age AS STRING) FROM CompUser AS u WHERE 
u.age = 23";
+       
+       List result = em.createQuery(query).getResultList();
+       
+       assertEquals(1, result.size());
+       assertEquals("23", result.get(0));
+       
+       endEm(em);
+    }
 
     public CompUser createUser(String name, String cName, Address add, int age,
         boolean isMale) {

Reply via email to