You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by rz...@apache.org on 2026/08/20 15:45:22 UTC
(openjpa) 01/01: [OPENJPA-2966] Use a dedicated cast target type for long values
This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch OPENJPA-2966
in repository https://gitbox.apache.org/repos/asf/openjpa.git
commit 46bed3f60fd376f15a82521450bb0ee68076e36f
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Thu Aug 20 17:40:01 2026 +0200
[OPENJPA-2966] Use a dedicated cast target type for long values
CAST(x AS LONG) was rendered with dict.decimalTypeName. An unsized DECIMAL is
not a 64 bit type on most databases: it is DECIMAL(10,0) on MySQL/MariaDB
(values above 9999999999 are silently clamped, only a SQL warning is raised),
DECIMAL(5,0) on Derby and precision 18 on SQL Server (both raise a range
error). So the current code either truncates or fails for ordinary long values.
bigintTypeName cannot simply be reused, and it must not be redefined either:
it is a DDL type name. DBDictionary.getTypeName(int) returns it for
Types.BIGINT, which drives CREATE TABLE/ADD COLUMN and the schema comparison in
Column.equalsColumn(). Changing it (for instance to DECIMAL(30) on
MySQL/MariaDB) would change the column type of every long mapping, break
AUTO_INCREMENT identity columns and report every existing BIGINT column as
incompatible. The dictionaries that do override bigintTypeName do so because
their database has no BIGINT column type at all (DB2 z/OS v8, Sybase, Informix).
Instead this follows the cast hook idiom already present in DBDictionary
(castFunction, typecastToStringTypeName, supportsUnsizedCharOnCast,
integerCastTypeName):
* new DBDictionary.longCastTypeName, defaulting to null. It is resolved lazily
through getLongCastTypeName() to bigintTypeName. It is deliberately not
initialised as "= bigintTypeName": field initializers run before subclass
constructors, so such a default would freeze to "BIGINT" and ignore
dictionaries that assign bigintTypeName in their constructor or in
connectedConfiguration() (DB2Dictionary does the latter for z/OS v8).
* new DBDictionary.getNumberCastTypeName(Class) and getStringCastTypeName(),
which is where the private sanitize() of TypecastAsNumber now lives; both
strip the DDL size marker {0} via the existing insertSize() helper.
TypecastAsNumber and TypecastAsString both use them, so the two siblings no
longer diverge.
* MySQLDictionary and MariaDBDictionary set longCastTypeName = "SIGNED".
Neither accepts BIGINT as a CAST target; SIGNED [INTEGER] is their 64 bit
signed cast target, mirroring the existing integerCastTypeName = "SIGNED".
Rendering changes: long casts now use BIGINT instead of DECIMAL on every
dictionary that does not override the field (SIGNED on MySQL/MariaDB, unchanged
NUMBER on Oracle since its bigintTypeName is NUMBER{0} as well). Applying the
size marker stripping to the string path also changes one dictionary
deliberately: FoxPro leaves supportsUnsizedCharOnCast at true while its
varcharTypeName is "CHARACTER{0}", so it emitted the literal
CAST(x AS CHARACTER{0}); it now emits CAST(x AS CHARACTER). No other dictionary
carries a marker in that branch, so no other string cast changes.
Not changed, reported instead: integerCastTypeName has the same field
initializer problem and is frozen to "INTEGER" for all dictionaries, ignoring
the integerTypeName overrides of Oracle, SQL Server and others. Making it lazy
would change the emitted int cast on those databases, which is out of scope
here.
Tested by openjpa-jdbc TestCastTypeNames, a dictionary level assertion on the
rendered cast target that needs no database, so MySQL, MariaDB, Oracle and DB2
are covered by the normal build. Against the previous code the long cases
render DECIMAL (and FoxPro CHARACTER{0}), so the test fails without the fix.
---
.../openjpa/jdbc/kernel/exps/TypecastAsNumber.java | 21 +-----
.../openjpa/jdbc/kernel/exps/TypecastAsString.java | 6 +-
.../org/apache/openjpa/jdbc/sql/DBDictionary.java | 54 ++++++++++++++
.../apache/openjpa/jdbc/sql/MariaDBDictionary.java | 2 +
.../apache/openjpa/jdbc/sql/MySQLDictionary.java | 2 +
.../apache/openjpa/jdbc/sql/TestCastTypeNames.java | 84 ++++++++++++++++++++++
6 files changed, 144 insertions(+), 25 deletions(-)
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 d7c44faa1..4558e228f 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 class TypecastAsNumber extends AbstractVal {
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 class TypecastAsNumber extends AbstractVal {
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 a4fb69b2a..1db505ea7 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 class TypecastAsString extends AbstractVal {
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 95f6bb573..0c549c7bc 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 class DBDictionary
public boolean supportsUnsizedCharOnCast = true;
+ /**
+ * Type name used as the target of a CAST to a 32 bit integer.
+ * <p>
+ * Note: this is a field initializer, so it is evaluated before any subclass constructor runs.
+ * Dictionaries which change {@link #integerTypeName} do <em>not</em> 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.
+ * <p>
+ * If <code>null</code> (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<String, IdentifierRule> namingRules = new HashMap<>();
@@ -2223,6 +2240,43 @@ public class DBDictionary
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 (<code>{0}</code>) 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 (<code>{0}</code>) 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 f1c5d834b..a88fe887f 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 class MariaDBDictionary extends DBDictionary {
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 11dda2698..be69d0b39 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 class 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 000000000..f9f89fd79
--- /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());
+ }
+}