Bug #22290 Calling SP with BigDecimal param set as BigDecimal(string) calls setString
Submitted: 13 Sep 2006 0:28 Modified: 27 Sep 2006 1:32
Reporter: Ben Anderson Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / J Severity:S3 (Non-critical)
Version:3.1.13 and 5.0.3 OS:Microsoft Windows (Windows XP)
Assigned to: CPU Architecture:Any
Tags: decimal, jdbc, truncation

[13 Sep 2006 0:28] Ben Anderson
Description:
When adding particular decimal(10,2) values together in an 'update' statement that stores the result in a decimal(10,2) column, a com.mysql.jdbc.MysqlDataTruncation error is thrown by the JDBC driver.

For example, I have a 'cost' column in an InnoDB table (named test) that has been defined as decimal(10,2). I have one row in the table with a 'cost' value of 1.00.

I attempt to add '1.11' to the current value of the 'cost' column via an update statement using JDBC i.e. update test set cost = cost + ? where id = ?

The following error is returned from the JDBC driver:
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data truncated for column 'cost' at row 1
	at com.mysql.jdbc.SQLError.convertShowWarningsToSQLWarnings(SQLError.java:709)
	at com.mysql.jdbc.MysqlIO.scanForAndThrowDataTruncation(MysqlIO.java:3461)
	at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1198)
	at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:677)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1357)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1274)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1259)

Note that the update works without generating an exception for all even decimal(10,2) values that I've tried - but adding an odd decimal(10,2) value will nearly always generate exception, particularly if the current value of 'cost' is odd.

I'm using the current GA release of the server (5.0.24) under Windows XP with an application that uses the production (3.1.13) Connector/J JDBC driver.

How to repeat:
Create the following table:

CREATE TABLE  `bentest`.`test` (
  `id` int(11) NOT NULL default '1',
  `cost` decimal(10,2) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Prime the table with the following row:

INSERT INTO `bentest`.`test` (`id`,`cost`) VALUES 
 (1,'1.00');

Run the following java program:

public class MyBug {

    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver").newInstance();
            conn = DriverManager
                    .getConnection("jdbc:mysql://localhost:3306/bentest"
                            + "?user=bentest&password=password");
            PreparedStatement ps = conn.prepareStatement(//
                    "update test set cost = cost + ? where id = 1");
            ps.setBigDecimal(1, new BigDecimal("1.11"));
            ps.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e) {
                }
            }
        }
    }

}

Re-run the test class using BigDecimal values of 1.10 or 1.12 (these values work ok - only odd values appear to cause the problem).

Suggested fix:
Setting the jdbcCompliantTruncation=false will prevent the JDBC driver from throwing an exception.

Modifying the update statement to truncate the result of the addition also works (although is an unacceptable workaround for our application) 

i.e. "update test set cost = truncate(cost + ?, 2) where id = 1"

Note that the following modification does not work (i.e. data truncation exception still occurs):

"update test set cost = cost + truncate(?, 2) where id = 1"

This suggests that the problem is not related to how the BigDecimal parameter is being passed to the server (changing the java prepared statement handling to use setString instead of setBigDecimal has no effect either).
[13 Sep 2006 0:44] Ben Anderson
I suppose my main point is that adding two values with a scale of 2 together will always produce a result with a scale 2. I don't see how truncation can occur in this scenario so I'm interested to know why it is being reported.
[15 Sep 2006 2:26] Ben Anderson
Test case that extends BaseBugReport

Attachment: TruncationBugReport.java (text/x-java), 1.95 KiB.

[15 Sep 2006 3:34] Ben Anderson
The same behaviour is observed when using Connector/J 5.0.3. The MySQL server version is 5.0.24.
[17 Sep 2006 23:43] Ben Anderson
Setting 'useServerPrepStmts=false' on the JDBC connection also prevents the problem occurring - although it sounds like we would be hurting performance by turning this off.
[19 Sep 2006 20:13] Tonci Grgin
Test case

Attachment: TestBug22290.java (text/x-java), 1.18 KiB.

[19 Sep 2006 20:21] Tonci Grgin
Hi Ben and thanks for excellent problem report. We do appreciate it.

I was unable to repeat the problem reported with attached test case on:
  - MySQL 5.0.24 BK on WinXP Pro SP2 localhost
  - Both 3.1 and 5.0 connector/J (SVN)
  - JDK 1.5.07
  - All connect options tested as you suggested, my final connect string was:
  &jdbcCompliantTruncation=true&useServerPrepStmts=true

Results:

mysql> select * from testbug22290;
+----+------+
| id | cost |
+----+------+
|  1 | 2.11 |
+----+------+
1 row in set (0.00 sec)

--
Done.

Connected to 5.0.24-log
Query to be executed: com.mysql.jdbc.ServerPreparedStatement[1] - update testbug22290 set cost = cost + '1.11' where id = 1

Time: 1,156

OK (1 test)

Ben, if you see anything in my test case that should be changed in order to repeat problem, please reopen the report.
[20 Sep 2006 0:30] Ben Anderson
MySQL initialisation file

Attachment: my.ini (application/octet-stream, text), 9.03 KiB.

[20 Sep 2006 0:34] Ben Anderson
Tonci, 

I tried your test case and the issue is still there. I should have included the my.ini file that I was using (I've now attached it).

The sql-mode I am using is "STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"

I could make your test case pass I've I removed the "STRICT_TRANS_TABLES" option. Could you recheck your test case with the STRICT_TRANS_TABLES option set?

Ben
[20 Sep 2006 7:32] Tonci Grgin
Ben, your report still makes perfect sense to me but I wasn't able to repeat it yesterday even with STRICT_TRANS_TABLES as you can see from my log:

060919 22:08:19	      7 Connect     root@localhost on test
		      7 Query       SET NAMES latin1
		      7 Query       SET character_set_results = NULL
		      7 Query       SHOW VARIABLES
		      7 Query       SHOW COLLATION
		      7 Query       SET autocommit=1
		      7 Query       SET sql_mode='STRICT_TRANS_TABLES'
		      7 Query       SELECT VERSION()
		      7 Query       DROP TABLE IF EXISTS testbug22290
		      7 Query       CREATE TABLE testbug22290 (`id` int(11) NOT NULL default '1',`cost` decimal(10,2) NOT NULL,PRIMARY KEY  (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8
		      7 Query       INSERT INTO testbug22290 (`id`,`cost`) VALUES (1,'1.00')
		      7 Prepare     [1] 
060919 22:08:29	      7 Execute     [1] update testbug22290 set cost = cost + '1.11' where id = 1
060919 22:08:52	      8 Connect     root@localhost on test
060919 22:09:00	      8 Query       select * from testbug22290
060919 22:09:13	      7 Query       DROP TABLE IF EXISTS testbug22290
		      7 Quit       
Then I tried more...
We introduced 'STRICT_TRANS_TABLES' server mode because it's much more efficient than checking warnings after every statement for truncations.
From the manual:
"If a numeric expression contains any strings, they are converted to double-precision floating-point values and the expression is approximate."
Your test fail in mysql cl client (expected) so I changed code to use numbers instead of strings and test passes (jdbcCompliantTruncation and useServerPrepStmts *both* true):
Done.

Connected to 5.0.24-log
Query to be executed: com.mysql.jdbc.ServerPreparedStatement[1] - update testbug22290 set cost = cost + '1.1100000000000000976996261670137755572795867919921875' where id = 1

Time: 0,579

OK (1 test)
--
mysql> update testbug22290 set cost = cost + '1.11' where id = 1;
Query OK, 1 row affected, 1 warning (0.03 sec)
Rows matched: 1  Changed: 1  Warnings: 1

mysql> show warnings;
+-------+------+-------------------------------------------+
| Level | Code | Message                                   |
+-------+------+-------------------------------------------+
| Note  | 1265 | Data truncated for column 'cost' at row 1 |
+-------+------+-------------------------------------------+
and
mysql> update testbug22290 set cost = cost + 1.11 where id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Concluding from this test, error you get has nothing to do with connector/J and/or stored procedures but with used server mode which requires more attention when writing queries.
[20 Sep 2006 8:02] Ben Anderson
Thanks for looking at this again Tonci but I don't understand your explanation. 

The only statement that I am sending to the server is via the prepared statement "update test set cost = cost + ? where id = 1"

I don't see where in my code the statement is being converted to "update test set cost = cost + '?' where id = 1" - this can only be happening in the JDBC driver or server itself surely?
[20 Sep 2006 8:07] Ben Anderson
In fact, it looks like your log highlights the problem:

 7 Query       INSERT INTO testbug22290 (`id`,`cost`) VALUES (1,'1.00')
		      7 Prepare     [1] 
060919 22:08:29	      7 Execute     [1] update testbug22290 set cost = cost +
'1.11' where id = 1
^^^^^^

Where did the single quotes come from? They were not part of my original prepared statement and should not be added by whatever is creating the update statement executed by the server.
[20 Sep 2006 8:55] Tonci Grgin
Ben, in your example you are sending string '1.11' to server in (setBigDecimal(1,"1.11")). Passing string value to c/J causes buffer type for parameter to be recognized as string type (253 I think) causing server to receive "1.11" instead of 1.11 which, in turn, triggers conversion STRING -> FLOAT triggering DATA TRUNCATION on the server in STRICT_TRANS_TABLES mode. Try using numbers like this:
this.pstmt.setBigDecimal(1, new BigDecimal(1.11));
System.out.println("Query to be executed: "+this.pstmt.toString());
...
[20 Sep 2006 9:02] Ben Anderson
Tonci,

In my test case and your test case, the same syntax has been used:

ps.setBigDecimal(1, new BigDecimal("1.11"));

This will always pass a BigDecimal value to the prepared statement, not a String.

Ben
[20 Sep 2006 9:06] Ben Anderson
Tonci, I also found that attempting to change the sql_mode via a 'set' command did not actually change the sql_mode. You need to change your my.ini file and restart the server for it to take effect. Could you try the test case with the my.ini file that I attached?
[20 Sep 2006 9:47] Tonci Grgin
Ben, I'm obviously not reaching you... Please, reread and reexamine my post.
 - I am using your my.ini file
 - I have rewrote test case to use setBigDecimal to pass decimal value, new BigDecimal(1.11), notice 1.11 *WITHOUT quotes*.
Everything works with compliant truncation and server-side prepared statements set to true.
The only question is whether parameter type buffer should be set to STRING when passing quoted value to setBigDecimal. If you want I can consult on this.
[20 Sep 2006 12:29] Ben Anderson
Tonci, your method of instanciating a BigDecimal value is imprecise. Here is what the java api documentation has to say about passing in a double to the BigDecimal constructor, as you are suggesting:

http://java.sun.com/j2se/1.4.2/docs/api/java/math/BigDecimal.html#BigDecimal(double)

BigDecimal
public BigDecimal(double val)
Translates a double into a BigDecimal. The scale of the BigDecimal is the smallest value such that (10scale * val) is an integer. 

Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances notwithstanding. 

The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.

...

The javadoc is telling me if I want a BigDecimal value that precisely represents 1.11 then I should be using the String constructor to do so.

I accept that you're not seeing the issue but I'm certain the problem is not related to how I am constructing the BigDecimal value that gets passed to setBigDecimal!
[20 Sep 2006 13:21] Tonci Grgin
Hi Ben. I agree with you but will have to change the synopsis.

Calling SP with DECIMAL param passed as STRING to setDecimal calls setString which makes MySQL server do STRING -> FLOAT (documented) conversion resulting in (expected) truncation warning. Test case and details attached to report.
I am not sure if this can be treated as bug at all but will ask for consult.
[20 Sep 2006 13:57] Mark Matthews
The JDBC driver is operating as expected. The only way to call a stored procedure and have prepared semantics (as required by JDBC) requires that the driver use the emulated client-side prepared statements. Since in this case, the values are eventually sent as a normal statement, the parameters are exposed to type conversions that might not happen in a server-side statement.

Prior versions of MySQL didn't do an intermediate float conversion (that I can remember) when it was known that the value was a DECIMAL, if the value was supplied as a string (it was string->decimal), and sending as a bare number actually caused conversion in some cases).

We'll have to test if sending as a bare number is now a better solution, but Tonci should investigate whether the intermediate conversion is a bug (in my opinion it is, the server knows the parameter is a DECIMAL, and therefore should know that it can go direct from string -> DECIMAL).
[20 Sep 2006 18:23] Keith Winton
We have recently tried to use the 5.0.3 driver for a customer, and most all of our application's prepared statements fail with that driver because of this error.  The DB columns are decimal(15,5)  (just fyi, to mirror how they exist on other db platforms, like the IBM iSeries), and the DAO objects do a setDouble() method (internal java object is a double).  All flavors of IBM db's, and MS-SQL all work find, as do the prior Connector/J versions customer was using

In general we want to use strict db checking, so we will have to tell this customer not to upgrade the driver, and hope it gets resolved.
[26 Sep 2006 19:22] Bugs System
A patch for this bug has been committed. After review, it may
be pushed to the relevant source trees for release in the next
version. You can access the patch from:

  http://lists.mysql.com/commits/12565
[26 Sep 2006 19:32] Mark Matthews
Fixed for 5.0.4/5.1.0, see nightly snapshots after 00:00 GMT Sep-27 at http://downloads.mysql.com/snapshots.php#connector-j if you want to test before release.
[27 Sep 2006 1:32] Ben Anderson
Great! - thanks for the work on this one guys.
[1 Oct 2006 10:16] Tonci Grgin
Continued in BUG#22869