Attached is a patch that handles the negative scale for a BigDecimal
that can occur in J2SE 5.0 onwards.
I would appreciate other eyes looking at it to ensure the handling of
negative scale is correct.
Previously BigDecimals always had a zero or postive scale. A negative
scale means that the value is the unscaled value * 10^(-scale).
// e.g. 1000 can be represented by:
// a BigDecimal with scale -3 (unscaled value of 1)
// or a BigDecimal with scale 0 (unscaled value of 1000)
The change is really two parts
1) Ensure that the SQL precision and scale (e.g. DECIMAL(5,2) has SQL
precision 5 and scale 2) is calculated correctly in J2SE 5.0 from a
BigDecimal value. This includes using the new precision method when
running in J2SE 5.0 and handling negative scale values correctly.
Comments were also added to clarify that methods were returning the
SQL precision and scale, and this differs for BigDecimal's definition
precision and scale.
e.g. the above BigDecimal values representing 1000 both have SQL
precision and scale of (3,0), but have BigDecimal precision and scale of
(1,-3) and (3,0).
2) Define and implement that the on-disk format of a DECIMAL for Derby
always has a zero or positive scale, so that all values written to disk
are independent of the JVM being used. So a database created by J2SE 5.0
is portable to J2SE 1.3/1.4.
It passes all the tests under jdk 1.4 and jdk 1.5, though I need to
verify the jdk 1.5 test runs as I had to halt it late in the run.
I'll commit it sometime in the next week, addressing any comments.
Thanks,
Dan.
M java\engine\org\apache\derby\iapi\types\SQLDecimal.java
M java\engine\org\apache\derby\iapi\types\NumberDataValue.java
Index: java/engine/org/apache/derby/iapi/types/SQLDecimal.java
===================================================================
--- java/engine/org/apache/derby/iapi/types/SQLDecimal.java (revision
165024)
+++ java/engine/org/apache/derby/iapi/types/SQLDecimal.java (working copy)
@@ -343,6 +343,7 @@
}
private static final Method toPlainString;
+ private static final Method bdPrecision;
static {
Method m;
try {
@@ -351,6 +352,12 @@
m = null;
}
toPlainString = m;
+ try {
+ m = BigDecimal.class.getMethod("precision", null);
+ } catch (NoSuchMethodException e) {
+ m = null;
+ }
+ bdPrecision = m;
}
public Object getObject()
@@ -425,8 +432,8 @@
/**
* Distill the BigDecimal to a byte array and
* write out: <UL>
- * <LI> scale (int) </LI>
- * <LI> length of byte array </LI>
+ * <LI> scale (zero or positive) as a byte </LI>
+ * <LI> length of byte array as a byte</LI>
* <LI> the byte array </LI> </UL>
*
*/
@@ -441,12 +448,43 @@
if (value != null) {
scale = value.scale();
+
+ // J2SE 5.0 introduced negative scale value for
BigDecimals.
+ // In previouse Java releases a negative scale was not
allowed
+ // (threw an exception on setScale and the constructor
that took
+ // a scale).
+ //
+ // Thus the Derby format for DECIMAL implictly assumed a
+ // positive or zero scale value, and thus now must
explicitly
+ // be positive. This is to allow databases created
under J2SE 5.0
+ // to continue to be supported under JDK 1.3/JDK 1.4,
ie. to continue
+ // the platform independence, independent of OS/cpu and
JVM.
+ //
+ // If the scale is negative set the scale to be zero,
this results
+ // in an unchanged value with a new scale. A BigDecimal
with a
+ // negative scale by definition is a whole number.
+ // e.g. 1000 can be represented by:
+ // a BigDecimal with scale -3 (unscaled value of 1)
+ // or a BigDecimal with scale 0 (unscaled value of 1000)
+
+ if (scale < 0) {
+ scale = 0;
+ value = value.setScale(0);
+ }
+
BigInteger bi = value.unscaledValue();
byteArray = bi.toByteArray();
} else {
scale = rawScale;
byteArray = rawData;
}
+
+ if (SanityManager.DEBUG)
+ {
+ if (scale < 0)
+ SanityManager.THROWASSERT("DECIMAL scale at
writeExternal is negative "
+ + scale + " value " + toString());
+ }
out.writeByte(scale);
out.writeByte(byteArray.length);
@@ -1051,11 +1089,9 @@
{
if (isNull())
return this;
-
- // the getWholeDigits() call will ensure via getBigDecimal()
- // that the rawData is translated into the BigDecimal in value.
+
if (desiredPrecision != IGNORE_PRECISION &&
- ((desiredPrecision - desiredScale) < getWholeDigits()))
+ ((desiredPrecision - desiredScale) <
SQLDecimal.getWholeDigits(getBigDecimal())))
{
throw
StandardException.newException(SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE,
("DECIMAL/NUMERIC("+desiredPrecision+","+desiredScale+")"));
@@ -1065,31 +1101,42 @@
return this;
}
+ /**
+ * Return the SQL scale of this value, number of digits after the
+ * decimal point, or zero for a whole number. This does not match the
+ * return from BigDecimal.scale() since in J2SE 5.0 onwards that can
return
+ * negative scales.
+ */
public int getDecimalValuePrecision()
{
- return getPrecision(getBigDecimal());
- }
- /**
- *
- * @param decimalValue the big decimal
- *
- * @return the precision
- */
- private static int getPrecision(BigDecimal decimalValue)
- {
- if ((decimalValue == null) ||
- decimalValue.equals(ZERO))
- {
+ if (isNull())
return 0;
- }
+
+ BigDecimal localValue = getBigDecimal();
- return SQLDecimal.getWholeDigits(decimalValue) +
decimalValue.scale();
+ return SQLDecimal.getWholeDigits(localValue) +
getDecimalValueScale();
}
+ /**
+ * Return the SQL scale of this value, number of digits after the
+ * decimal point, or zero for a whole number. This does not match the
+ * return from BigDecimal.scale() since in J2SE 5.0 onwards that can
return
+ * negative scales.
+ */
public int getDecimalValueScale()
{
+ if (isNull())
+ return 0;
+
BigDecimal localValue = getBigDecimal();
- return (localValue == null) ? 0 : localValue.scale();
+
+ int scale = localValue.scale();
+ if (scale >= 0)
+ return scale;
+
+ // BigDecimal scale is negative, so number must have no
fractional
+ // part as its value is the unscaled value * 10^-scale
+ return 0;
}
/**
@@ -1125,19 +1172,14 @@
}
}
- private int getWholeDigits()
- {
- return SQLDecimal.getWholeDigits(getBigDecimal());
- }
-
+ /**
+ * Calculate the number of digits to the left of the decimal point
+ * of the passed in value.
+ * @param decimalValue Value to get whole digits from, never null.
+ * @return number of whole digits.
+ */
private static int getWholeDigits(BigDecimal decimalValue)
{
- if ((decimalValue == null) ||
- decimalValue.equals(ZERO))
- {
- return 0;
- }
-
/**
* if ONE > abs(value) then the number of whole digits is 0
*/
@@ -1146,7 +1188,36 @@
{
return 0;
}
-
+
+ if (JVMInfo.JDK_ID >= JVMInfo.J2SE_15)
+ {
+ // use reflection so we can still compile using JDK1.4
+ // if we are prepared to require 1.5 to compile then
this can be a
+ // direct call
+ try {
+ // precision is the number of digits in the
unscaled value,
+ // subtracting the scale (positive or negative)
will give the
+ // number of whole digits.
+ int precision = ((Integer)
bdPrecision.invoke(decimalValue,
+ null)).intValue();
+ return precision - decimalValue.scale();
+ } catch (IllegalAccessException e) {
+ // can't happen based on the JDK spec
+ throw new IllegalAccessError("precision");
+ } catch (InvocationTargetException e) {
+ Throwable t = e.getTargetException();
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else if (t instanceof Error) {
+ throw (Error) t;
+ } else {
+ // can't happen
+ throw new
IncompatibleClassChangeError("precision");
+ }
+ }
+
+ }
+
String s = decimalValue.toString();
return (decimalValue.scale() == 0) ? s.length() : s.indexOf('.');
}
Index: java/engine/org/apache/derby/iapi/types/NumberDataValue.java
===================================================================
--- java/engine/org/apache/derby/iapi/types/NumberDataValue.java
(revision 165024)
+++ java/engine/org/apache/derby/iapi/types/NumberDataValue.java
(working copy)
@@ -196,14 +196,18 @@
public void setValue(Number theValue) throws StandardException;
/**
- Return the precision of this specific DECIMAL value.
+ Return the SQL precision of this specific DECIMAL value.
+ This does not match the return from BigDecimal.precision()
+ added in J2SE 5.0, which represents the precision of the
unscaled value.
If the value does not represent a SQL DECIMAL then
the return is undefined.
*/
public int getDecimalValuePrecision();
/**
- Return the scale of this specific DECIMAL value.
+ Return the SQL scale of this specific DECIMAL value.
+ This does not match the return from BigDecimal.scale()
+ since in J2SE 5.0 onwards that can return negative scales.
If the value does not represent a SQL DECIMAL then
the return is undefined.
*/