package cz.michal.database.test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; /** * Test demonstrating the bug in Connector/J when OffsetDateTime is used. * *
CREATE DATABASE test; USE test; CREATE TABLE test ( dt DATETIME NOT NULL, ts TIMESTAMP NOT NULL ); INSERT INTO test VALUES( NOW(), NOW() ); **/ public class MySQLOffsetDateTimeTest { /** * Database host to connect to. */ private static final String HOST = "FIXME"; /** * Username to connect to the database. */ private static final String USER_NAME = "FIXME"; /** * Password to connect to the database. */ private static final String PASSWORD = "FIXME"; /** * Database to connect to. */ private static final String DATABASE = "test"; /** * Main application entry point. */ public static void main( String[] arguments ) { MySQLOffsetDateTimeTest test = new MySQLOffsetDateTimeTest(); try { test.run(); System.out.println( "Demonstration complete." ); } catch ( SQLException e ) { System.out.println( "Exception thrown. This test isn't supposed to do it." ); } } /** * Run the test. */ private void run() throws SQLException { // Connect to the database. connect(); // Set timezone to something else than UTC. // See the comment at loadOffsetDateTimeFromTimestamp() for an explanation why we store the zone. Statement statement = connection.createStatement(); statement.execute( "SET time_zone = '+10:00'" ); zone = TimeZone.getTimeZone( "GMT+10" ); // Load the first column of the TEST table. statement = connection.createStatement(); ResultSet row = statement.executeQuery( "SELECT * FROM test" ); row.next(); // Attempt to parse the columns as OffsetDateTime. tryOffsetDateTime( row, "dt" ); tryOffsetDateTime( row, "ts" ); } /** * Main part of the test, including an explanation */ private void tryOffsetDateTime( ResultSet row, String column ) throws SQLException { // Print the string value for comparison. String stringValue = row.getString( column ); System.out.println( "String value of column " + column + " = '" + stringValue + "'." ); // Attempt to retrieve the OffsetDateTime. try { @SuppressWarnings( "unused" ) OffsetDateTime date = row.getObject( column, OffsetDateTime.class ); } catch ( SQLException e ) { System.out.println( "getObject( ..., OffsetDateTime.class ) failed with an exception." ); e.printStackTrace(); } // Explanation why this fails: // // 1. The DATETIME or TIMESTAMP columns are returned by MySQL in the format "2019-02-24 20:59:27". // // 2. Connector/J contains support for OffsetDateTime, which looks like this: // (Code taken from ResultSetImpl.getObject, line 1446 and on) // } else if (type.equals(OffsetDateTime.class)) { // try { // String odt = getString(columnIndex); // return odt == null ? null : (T) OffsetDateTime.parse(odt); // } catch (DateTimeParseException e) { // // Let it continue and try by object deserialization. // } // // 3. call to GetObject( OffsetDateTime.class ) gets inside the if. // odt string is retrieved, in the format "2019-02-24 20:59:27" // OffsetDateTime.parse() however expects a different format "2007-12-03T10:15:30+01:00" // OffsetDateTime.parse() throws an exception, which is caught in the code - and ResultSetImpl.getObject() // continues to its end, where it does // throw SQLError.createSQLException("Conversion not supported for type " + type.getName(), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, // getExceptionInterceptor()); // // Some comments what could be done about it: // A. For DateTime columns: they don't store any timezone information. // The only way how to convert the value to OffsetDateTime is to assume some timezone - say UTC. // Conversion from a string in the format "2019-02-24 20:59:27" can be done using // LocalDateTime local = resultSet.getObject( column, LocalDateTime.class ); // return ( local != null ) ? local.atOffset( ZoneOffset.UTC ) : null; // B. For TIMESTAMP, which are printed in the timezone specific to the connection, // the conversion would need to take into account that timezone. // this.session.getServerSession().getDefaultTimeZone() // // See the two attempts at implementations (outside of the ResultSetImpl, so using some hacks) below. OffsetDateTime offset; if ( "dt".equals( column ) ) { offset = loadOffsetDateTimeFromDateTime( row, column ); } else { offset = loadOffsetDateTimeFromTimestamp( row, column ); } System.out.println( "When loaded using a hack, the result is " + offset.toString() + ". In UTC, this is " + offset.withOffsetSameInstant( ZoneOffset.UTC ) + '.' ); } /** * Time zone used by the connection. *
* I can't get the actual time zone from ResultSetImpl, so I just manually create it here. */ private TimeZone zone; /** * Sample how ResultSetImpl.getObject( OffsetDateTime.class ) could be done for a DATETIME column. */ private OffsetDateTime loadOffsetDateTimeFromDateTime( ResultSet row, String column ) throws SQLException { LocalDateTime local = row.getObject( column, LocalDateTime.class ); return local.atOffset( ZoneOffset.UTC ); } /** * Sample how ResultSetImpl.getObject( OffsetDateTime.class ) could be done for a TIMESTAMP column. *
* Note that this function requires TimeZone, which needs to be the TimeZone that the connection uses. * This is available inside ResultSetImpl using *
* this.session.getServerSession().getDefaultTimeZone())
*
* (e.g. inside ResultSetImpl.getTime() ), but I can't get it here - so I'll just create a random one
* that matches my connection timezone. This is just for a demonstration.
*/
private OffsetDateTime loadOffsetDateTimeFromTimestamp( ResultSet row, String column ) throws SQLException
{
int zoneOffsetMilliseconds = zone.getOffset( System.currentTimeMillis() );
int zoneOffsetSeconds = zoneOffsetMilliseconds / 1000;
ZoneOffset offset = ZoneOffset.ofTotalSeconds( zoneOffsetSeconds );
LocalDateTime local = row.getObject( column, LocalDateTime.class );
return local.atOffset( offset );
}
/**
* The connection to the database.
*/
private Connection connection;
/**
* Connect to the database.
*/
private void connect() throws SQLException
{
Properties properties = new Properties();
properties.setProperty( "user", USER_NAME );
properties.setProperty( "password", PASSWORD );
String url = "jdbc:mysql://" + HOST + "/" + DATABASE;
connection = DriverManager.getConnection( url, properties );
}
}