From b7a45f5825ae6e3cb1bf9dcdbc7defb9d83e9b37 Mon Sep 17 00:00:00 2001 From: Richard Zowalla Date: Thu, 20 Aug 2026 17:40:01 +0200 Subject: [PATCH 1/3] [OPENJPA-2966] Use a dedicated cast target type for long values Long values are now cast via a lazily resolved longCastTypeName (SIGNED on MySQL/MariaDB) instead of the DDL decimalTypeName, which defaults to DECIMAL(10,0) there and silently truncates, and both cast paths now strip the size suffix through the same DBDictionary helpers. --- .../jdbc/kernel/exps/TypecastAsNumber.java | 21 +---- .../jdbc/kernel/exps/TypecastAsString.java | 6 +- .../apache/openjpa/jdbc/sql/DBDictionary.java | 54 ++++++++++++ .../openjpa/jdbc/sql/MariaDBDictionary.java | 2 + .../openjpa/jdbc/sql/MySQLDictionary.java | 2 + .../openjpa/jdbc/sql/TestCastTypeNames.java | 84 +++++++++++++++++++ 6 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsNumber.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsNumber.java index d7c44faa14..4558e228f7 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsNumber.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/TypecastAsNumber.java @@ -160,7 +160,7 @@ public void appendTo(Select sel, ExpContext ctx, ExpState state, SQLBuffer sql, sql.append(part1); _val.appendTo(sel, ctx, etnstate.valueState, sql, 0); sql.append(part2); - sql.append(getDbNumberTargetTypeName(dict)); + sql.append(dict.getNumberCastTypeName(getType())); sql.append(part3); } @@ -175,23 +175,4 @@ public void acceptVisit(ExpressionVisitor visitor) { public int getId() { return Val.EXTRACTDTF_VAL; } - - private static String sanitize(String type) { - final int idx = type.indexOf('{'); - return idx < 0 ? type : type.substring(0, idx); - } - - private String getDbNumberTargetTypeName(DBDictionary dict) { - String type; - if (getType() == int.class) { - type = dict.integerCastTypeName; - } else if (getType() == long.class) { - type = dict.decimalTypeName; - } else if (getType() == float.class) { - type = dict.floatTypeName; - } else { - type = dict.doubleTypeName; - } - return sanitize(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 index a4fb69b2a2..1db505ea77 100644 --- 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 @@ -148,11 +148,7 @@ public void appendTo(Select sel, ExpContext ctx, ExpState state, sql.append(part1); _val.appendTo(sel, ctx, casstate.valueState, sql, 0); sql.append(part2); - if (dict.supportsUnsizedCharOnCast) { - sql.append(dict.varcharTypeName); - } else { - sql.append(dict.typecastToStringTypeName + "(" + dict.characterColumnSize + ")"); - } + sql.append(dict.getStringCastTypeName()); sql.append(part3); } 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 95f6bb573a..0c549c7bc6 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 @@ -511,8 +511,25 @@ public enum DateMillisecondBehaviors { DROP, ROUND, RETAIN } public boolean supportsUnsizedCharOnCast = true; + /** + * Type name used as the target of a CAST to a 32 bit integer. + *

+ * Note: this is a field initializer, so it is evaluated before any subclass constructor runs. + * Dictionaries which change {@link #integerTypeName} do not implicitly change this value. + * Set it explicitly if the DDL type name is not a valid CAST target. + */ public String integerCastTypeName = integerTypeName; + /** + * Type name used as the target of a CAST to a 64 bit integer. + *

+ * If null (the default), {@link #bigintTypeName} is resolved lazily by + * {@link #getLongCastTypeName()}. Resolving lazily rather than in a field initializer is deliberate: + * several dictionaries assign {@link #bigintTypeName} in their constructor or even in + * {@link #connectedConfiguration(java.sql.Connection)}, which happens after field initialization. + */ + public String longCastTypeName = null; + // Naming utility and naming rules private DBIdentifierUtil namingUtil = null; private final Map namingRules = new HashMap<>(); @@ -2223,6 +2240,43 @@ protected int getDateFractionDigits(Column col, String typeName) { return dateFractionDigits; } + /** + * Return the type name to use as the target of a CAST to a 64 bit integer. + * Defaults to {@link #bigintTypeName} unless {@link #longCastTypeName} was set explicitly. + */ + public String getLongCastTypeName() { + return longCastTypeName != null ? longCastTypeName : bigintTypeName; + } + + /** + * Return the type name to use as the target of a CAST of a numeric value to the given java type. + * Any DDL size marker ({0}) is stripped, as CAST targets are not sized by the schema. + */ + public String getNumberCastTypeName(Class type) { + String name; + if (type == int.class || type == Integer.class) { + name = integerCastTypeName; + } else if (type == long.class || type == Long.class) { + name = getLongCastTypeName(); + } else if (type == float.class || type == Float.class) { + name = floatTypeName; + } else { + name = doubleTypeName; + } + return insertSize(name, null); + } + + /** + * Return the type name to use as the target of a CAST to a string. + * Any DDL size marker ({0}) is stripped. + */ + public String getStringCastTypeName() { + if (supportsUnsizedCharOnCast) { + return insertSize(varcharTypeName, null); + } + return insertSize(typecastToStringTypeName, null) + "(" + characterColumnSize + ")"; + } + /** * Helper method that inserts a size clause for a given SQL type. * diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MariaDBDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MariaDBDictionary.java index f1c5d834b3..a88fe887f9 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MariaDBDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MariaDBDictionary.java @@ -181,6 +181,8 @@ public MariaDBDictionary() { dateFractionDigits = 0; supportsUnsizedCharOnCast = false; + // MariaDB has no BIGINT cast target; SIGNED [INTEGER] yields a 64 bit signed value + longCastTypeName = "SIGNED"; } @Override diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MySQLDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MySQLDictionary.java index 11dda26988..be69d0b39a 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MySQLDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/MySQLDictionary.java @@ -180,6 +180,8 @@ public MySQLDictionary() { dateFractionDigits = 0; supportsUnsizedCharOnCast = false; integerCastTypeName = "SIGNED"; + // MySQL has no BIGINT cast target; SIGNED [INTEGER] yields a 64 bit signed value + longCastTypeName = "SIGNED"; } @Override diff --git a/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java new file mode 100644 index 0000000000..f9f89fd797 --- /dev/null +++ b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java @@ -0,0 +1,84 @@ +/* + * 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.sql; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Verifies the type names rendered as the target of a SQL CAST, see OPENJPA-2966. + */ +public class TestCastTypeNames { + + /** + * A 64 bit value must not be cast to an unsized DECIMAL: that defaults to DECIMAL(10,0) on + * MySQL/MariaDB (silent truncation) and to DECIMAL(5,0) on Derby (SQLState 22003). + */ + @Test + public void testLongCastTypeName() { + assertEquals("BIGINT", new DBDictionary().getNumberCastTypeName(long.class)); + assertEquals("BIGINT", new DerbyDictionary().getNumberCastTypeName(long.class)); + assertEquals("BIGINT", new H2Dictionary().getNumberCastTypeName(long.class)); + assertEquals("BIGINT", new PostgresDictionary().getNumberCastTypeName(long.class)); + assertEquals("BIGINT", new DB2Dictionary().getNumberCastTypeName(long.class)); + + // MySQL and MariaDB do not accept BIGINT as a CAST target, SIGNED [INTEGER] is 64 bit there + assertEquals("SIGNED", new MySQLDictionary().getNumberCastTypeName(long.class)); + assertEquals("SIGNED", new MariaDBDictionary().getNumberCastTypeName(long.class)); + + // size markers of the DDL type name must not leak into the CAST + assertEquals("NUMBER", new OracleDictionary().getNumberCastTypeName(long.class)); + } + + /** + * The cast type name is resolved lazily, so a dictionary which assigns its DDL type name after + * field initialisation (e.g. DB2 z/OS v8 in connectedConfiguration()) is honoured. + */ + @Test + public void testLongCastTypeNameIsResolvedLazily() { + DBDictionary dict = new DB2Dictionary(); + dict.bigintTypeName = "DECIMAL(31,0)"; + assertEquals("DECIMAL(31,0)", dict.getNumberCastTypeName(long.class)); + + dict.longCastTypeName = "SIGNED"; + assertEquals("SIGNED", dict.getNumberCastTypeName(long.class)); + } + + @Test + public void testOtherNumberCastTypeNames() { + assertEquals("INTEGER", new DBDictionary().getNumberCastTypeName(int.class)); + assertEquals("SIGNED", new MySQLDictionary().getNumberCastTypeName(int.class)); + assertEquals("FLOAT", new DBDictionary().getNumberCastTypeName(float.class)); + assertEquals("DOUBLE", new DBDictionary().getNumberCastTypeName(double.class)); + } + + /** + * The string cast must strip the DDL size marker as well - FoxPro used to render + * CAST(x AS CHARACTER{0}). + */ + @Test + public void testStringCastTypeName() { + assertEquals("VARCHAR", new DBDictionary().getStringCastTypeName()); + assertEquals("CHARACTER", new FoxProDictionary().getStringCastTypeName()); + assertEquals("CHAR(255)", new MySQLDictionary().getStringCastTypeName()); + assertEquals("CHAR(255)", new MariaDBDictionary().getStringCastTypeName()); + assertEquals("VARCHAR(255)", new OracleDictionary().getStringCastTypeName()); + } +} From fe17c1725616c7f8195d2c6f4117c7e05cf6a160 Mon Sep 17 00:00:00 2001 From: Maxim Solodovnik Date: Sun, 23 Aug 2026 11:34:34 +0700 Subject: [PATCH 2/3] [OPENJPA-2966] integerCastTypeName is lazily calculated --- .../apache/openjpa/jdbc/sql/DBDictionary.java | 33 +++++++++++++------ .../openjpa/jdbc/sql/TestCastTypeNames.java | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) 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 0c549c7bc6..da5cdd38ab 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 @@ -18,6 +18,8 @@ */ package org.apache.openjpa.jdbc.sql; +import static java.util.Locale.ROOT; + import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.CharArrayReader; @@ -142,8 +144,6 @@ import org.apache.openjpa.util.UnsupportedException; import org.apache.openjpa.util.UserException; -import static java.util.Locale.ROOT; - /** * Class which allows the creation of SQL dynamically, in a @@ -518,7 +518,7 @@ public enum DateMillisecondBehaviors { DROP, ROUND, RETAIN } * Dictionaries which change {@link #integerTypeName} do not implicitly change this value. * Set it explicitly if the DDL type name is not a valid CAST target. */ - public String integerCastTypeName = integerTypeName; + public String integerCastTypeName = null; /** * Type name used as the target of a CAST to a 64 bit integer. @@ -2240,12 +2240,20 @@ protected int getDateFractionDigits(Column col, String typeName) { return dateFractionDigits; } + /** + * Return the type name to use as the target of a CAST to a 32 bit integer. + * Defaults to {@link #integerTypeName} unless {@link #integerCastTypeName} was set explicitly. + */ + public String getIntegerCastTypeName() { + return integerCastTypeName == null ? integerTypeName : integerCastTypeName; + } + /** * Return the type name to use as the target of a CAST to a 64 bit integer. * Defaults to {@link #bigintTypeName} unless {@link #longCastTypeName} was set explicitly. */ public String getLongCastTypeName() { - return longCastTypeName != null ? longCastTypeName : bigintTypeName; + return longCastTypeName == null ? bigintTypeName : longCastTypeName; } /** @@ -2255,7 +2263,7 @@ public String getLongCastTypeName() { public String getNumberCastTypeName(Class type) { String name; if (type == int.class || type == Integer.class) { - name = integerCastTypeName; + name = getIntegerCastTypeName(); } else if (type == long.class || type == Long.class) { name = getLongCastTypeName(); } else if (type == float.class || type == Float.class) { @@ -5356,27 +5364,32 @@ public void startConfiguration() { @Override public void endConfiguration() { // add additional reserved words set by user - if (reservedWords != null) + if (reservedWords != null) { reservedWordSet.addAll(Arrays.asList(StringUtil.split(reservedWords.toUpperCase(Locale.ENGLISH), ",", 0))); + } // add system schemas set by user - if (systemSchemas != null) + if (systemSchemas != null) { systemSchemaSet.addAll(Arrays.asList(StringUtil.split(systemSchemas.toUpperCase(Locale.ENGLISH), ",", 0))); + } // add system tables set by user - if (systemTables != null) + if (systemTables != null) { systemTableSet.addAll(Arrays.asList(StringUtil.split(systemTables.toUpperCase(Locale.ENGLISH), ",", 0))); + } // add fixed size type names set by the user - if (fixedSizeTypeNames != null) + if (fixedSizeTypeNames != null) { fixedSizeTypeNameSet.addAll(Arrays.asList(StringUtil.split(fixedSizeTypeNames.toUpperCase(Locale.ENGLISH), ",", 0))); + } // if user has unset sequence sql, null it out so we know sequences // aren't supported nextSequenceQuery = StringUtil.trimToNull(nextSequenceQuery); - if (selectWords != null) + if (selectWords != null) { selectWordSet.addAll(Arrays.asList(StringUtil.split(selectWords.toUpperCase(Locale.ENGLISH), ",", 0))); + } if (invalidColumnWordSet.isEmpty()) { Collection invalidColumns = loadFromResource("sql-invalid-column-names.rsrc"); diff --git a/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java index f9f89fd797..018e8e51a8 100644 --- a/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java +++ b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java @@ -65,6 +65,7 @@ public void testLongCastTypeNameIsResolvedLazily() { public void testOtherNumberCastTypeNames() { assertEquals("INTEGER", new DBDictionary().getNumberCastTypeName(int.class)); assertEquals("SIGNED", new MySQLDictionary().getNumberCastTypeName(int.class)); + assertEquals("NUMBER", new OracleDictionary().getNumberCastTypeName(int.class)); // tests lazy inint assertEquals("FLOAT", new DBDictionary().getNumberCastTypeName(float.class)); assertEquals("DOUBLE", new DBDictionary().getNumberCastTypeName(double.class)); } From 249b36dc719ce05ae5128b4d8e1093a642ee0734 Mon Sep 17 00:00:00 2001 From: Richard Zowalla Date: Sat, 29 Aug 2026 19:07:55 +0200 Subject: [PATCH 3/3] [OPENJPA-2966] Cache resolved CAST target type names getNumberCastTypeName() and getStringCastTypeName() are called on every SQL generation and stripped the DDL size marker each time. Cache the stripped names, keyed by the DDL type name they were derived from, so a dictionary that assigns its type names in the constructor or in connectedConfiguration() still resolves correctly. --- .../apache/openjpa/jdbc/sql/DBDictionary.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) 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 da5cdd38ab..39c789ffc6 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 @@ -72,6 +72,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import javax.sql.DataSource; @@ -530,6 +531,14 @@ public enum DateMillisecondBehaviors { DROP, ROUND, RETAIN } */ public String longCastTypeName = null; + /** + * Cache of CAST target type names, keyed by the DDL type name they were derived from. + * The DDL type names themselves may still be changed by a subclass constructor or by + * {@link #connectedConfiguration(java.sql.Connection)}, so the key is the source name + * rather than the java type, and the cached value stays valid across such changes. + */ + private final Map castTypeNames = new ConcurrentHashMap<>(); + // Naming utility and naming rules private DBIdentifierUtil namingUtil = null; private final Map namingRules = new HashMap<>(); @@ -2271,7 +2280,7 @@ public String getNumberCastTypeName(Class type) { } else { name = doubleTypeName; } - return insertSize(name, null); + return castTypeName(name); } /** @@ -2280,9 +2289,17 @@ public String getNumberCastTypeName(Class type) { */ public String getStringCastTypeName() { if (supportsUnsizedCharOnCast) { - return insertSize(varcharTypeName, null); + return castTypeName(varcharTypeName); } - return insertSize(typecastToStringTypeName, null) + "(" + characterColumnSize + ")"; + return castTypeName(typecastToStringTypeName) + "(" + characterColumnSize + ")"; + } + + /** + * Strip any DDL size marker from the given type name so that it can be used as a CAST target. + * The result is cached, as CAST targets are resolved on every SQL generation. + */ + private String castTypeName(String typeName) { + return castTypeNames.computeIfAbsent(typeName, n -> insertSize(n, null)); } /**