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..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 @@ -511,7 +511,24 @@ public enum DateMillisecondBehaviors { DROP, ROUND, RETAIN } public boolean supportsUnsizedCharOnCast = true; - public String integerCastTypeName = integerTypeName; + /** + * 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 = null; + + /** + * 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; @@ -2223,6 +2240,51 @@ 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 ? bigintTypeName : longCastTypeName; + } + + /** + * 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 = getIntegerCastTypeName(); + } 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. * @@ -5302,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/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..018e8e51a8 --- /dev/null +++ b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestCastTypeNames.java @@ -0,0 +1,85 @@ +/* + * 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("NUMBER", new OracleDictionary().getNumberCastTypeName(int.class)); // tests lazy inint + 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()); + } +}