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) {