Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types support #17

Merged
merged 18 commits into from
Jan 19, 2021
Merged

Types support #17

merged 18 commits into from
Jan 19, 2021

Conversation

SeriyBg
Copy link
Contributor

@SeriyBg SeriyBg commented Dec 29, 2020

Fix #6
Fix #5
Fix #18

}

@Override
public double getDouble(int columnIndex) throws SQLException {
return get(columnIndex);
return convertTo(get(columnIndex), Double.class);
}

@Override
Copy link

@devozerov devozerov Dec 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BigDecimal with scale should be supported as well. It should be relatively trivial to implement provided that we already have conversions to BigDecimal.

}

@SuppressWarnings("unchecked")
static <T> T convertTo(Object object, Class<T> clazz) throws SQLException {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the value from the driver is a performance-sensitive operation. The current implementation looks too heavy to me, especially for simple types:

  1. For all methods except getObject, getDate, getTime and getTimestamp, we do not need to do any lookups. It should be sufficient to do the following. It requires only one hash map lookup as opposed to three in the current impl:
static <T> T convertTo(Object object, QueryDataType type) throws SQLException {
    try {
         return type.convert(object);
    } catch (Exception e) {
        // handle exception
    }
        }
}
  1. For date, time, timestamp: first invoke the method from p.1, then apply the subsequent conversion. One lookup instead of three.
  2. For object:
  • resolve the type of the passed class using QueryDataTypeUtils.resolveTypeForClass
  • invoke logic from p.1
  • if requested class is one of java.sql.Date, java.sql.Time, java.sql.Timestamp - invoke logic from p.2
  • check if the produced object is assignable to the requested class. If yes - return, otherwise - throw a proper exception (currently we will throw an implicit ClassCastException)

This way we have simple implementation for simple cases, and gradually make it heavier for more complex cases. We also eliminate two of three hash map lookups.

@@ -43,6 +43,8 @@
import java.util.Iterator;
import java.util.Map;

import static com.hazelcast.jdbc.TypeConverter.convertTo;

public class JdbcResultSet implements ResultSet {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getObject(int columnIndex, Class<T> type) and getObject(String columnLabel, Class<T> type) methods ignore the passed type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -43,6 +43,8 @@
import java.util.Iterator;
import java.util.Map;

import static com.hazelcast.jdbc.TypeConverter.convertTo;

public class JdbcResultSet implements ResultSet {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to JDBC spec, a somewhat similar approach to conversions is required for parameters (PreparedStatement.setObject(...)). Do we have it on our radars?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue #18

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class DriverTypeConversionTest {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, the test coverage looks insufficient. Are we sure that we test every possible pair of from/to types? This is tedious, but getting the results from the ResultSet is a foundational part of the driver, and we can't allow bugs here.
I would propose to test every type pair separately with a precise contract:

  1. If it should succeed, then what is the expected result?
  2. If it should fail, then check that only SQLException is thrown, and check for the specific error message (since error messages are part of the public API).

We did exactly the same for the server part. See com.hazelcast.sql.impl.type.converter.ConvertersTest and com.hazelcast.sql.impl.type.converter.CastFunctionIntegrationTest as examples of this exhaustive testing approach. Also, there is a set of convenient classes to return different types of values from the query: com.hazelcast.sql.support.expressions.ExpressionTypes, com.hazelcast.sql.support.expressions.ExpressionValue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added move test cases to cover from/to types pair. As well as changing the logic for conversion to the suggested one

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we could benefit from the mutation testing integration.

Example: https://github.com/hazelcast/hazelcast-zookeeper/pull/60/files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely. I will add it as a separate PR. Also created an issue for it #19

@SeriyBg SeriyBg requested a review from devozerov December 30, 2020 14:56
Copy link

@pivovarit pivovarit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like @devozerov covered the most important aspects already.

src/main/java/com/hazelcast/jdbc/TypeConverter.java Outdated Show resolved Hide resolved
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class DriverTypeConversionTest {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we could benefit from the mutation testing integration.

Example: https://github.com/hazelcast/hazelcast-zookeeper/pull/60/files

}

@Override
public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
throw JdbcUtils.unsupported("BigDecimal with scale is not supported");
return getBigDecimal(columnIndex).setScale(scale);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setScale method may throw an ArithmeticException, that we do not wrap into an SQLException.


@SuppressWarnings("unchecked")
static <T> T convertTo(Object object, QueryDataType targetDataType) throws SQLException {
return convertAs(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All conversion methods are on the hot path. I would avoid using lambdas here because every invocation creates a new lambda instance. It is better to have some code duplication here in favor of the speed.

Also please note that currently, we do two null checks instead of one: one check in the convertAs method, another in the QueryDataType.convert.

Another problem is that we do not consult column metadata. This forces us to call relatively heavy Converters.getConverter on the hot path. Instead, we may create a static table from SqlColumnType to QueryDataType, and use metadata to resolve the current column's type without Converters.getConverter. Also, this may lead to a situation, when, say, we have a null value for the column of TYPE_A, try to convert it to TYPE_B, and it pass. While in reality, these two types are incompatible (e.g. DATE and TIME cannot be converted to each other).

To summarize, we should do a minimal number of operations on the hot path, and make sure that we throw an exception for incompatible types even if the current value is null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to use SqlColumnType from metadata the conversion will be much more strict. For example, having an int field with the value 42 won't be possible to convert to String, for now, it is. Is this the expected behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested the behavior with H2, and there it is possible to call getString on DECIMAL field and conversion will be successful. And same for null values, having null as a String it is possible to successfully call getBigDecimal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

H2 is a toy database, we hardly should use it as a reference. In any case, if we have a DECIMAL column, then the associated converter BigDecimalConverter allows for conversion to string (BigDecimalConverter.asVarchar).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have tried with Postgres, and the behavior is the same: having a field of type date will null value I can successfully cal getTime which will return null. The same goes for String to BigDecimal. Should we follow the same behavior or do we decide to throw an exception for incompatible types even if the value can be converted?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we may have a null check, that return null for every combination of types, and never fail. And only if the value is not null we resort to converters.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, my comment is not valid:

Also, this may lead to a situation, when, say, we have a null value for the column of TYPE_A, try to convert it to TYPE_B, and it pass. While in reality, these two types are incompatible (e.g. DATE and TIME cannot be converted to each other).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. So for the null value, we will convert anyway, otherwise will convert only if it is possible. One more interesting thing for Postgres: having a null value for a String field will return 0 for the getLong call. Will follow the same behavior


@SuppressWarnings("unchecked")
static <T> T convertTo(Object object, int targetSqlType) throws SQLException {
QueryDataType queryDataType = SQL_TYPES_TO_QUERY_DATA_TYPE.get(targetSqlType);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we support FLOAT, NUMERIC, DECIMAL, DATE, TIME, TIMESTAMP, NULL, JAVA_OBJECT, CHAR types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, types DATE, TIME, TIMESTAMP, NULL, JAVA_OBJECT are not supported at the moment by IMDG SQL module, please correct me if I'm wrong. So I have created the issue to add it when it will be available in sql-module: #26 and #25
For all the other types will add the implementation

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are supported in a limited way - we may get only a column of this type, but cannot use these types in expressions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQL_TYPES_TO_QUERY_DATA_TYPE is used for PreparedStatement, where it's not supported atm. For the ResultSet we do support these types

try {
return supplier.get();
} catch (Exception e) {
throw new SQLException("Cannot convert '" + object + "' of type "

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many tools show only the message of the top exception. In the current implementation, the user will only see that we cannot convert one type to another, but will not see the reason. We'd better add the message of the e to the created exception.

}

static Timestamp convertToTimestamp(Object object) throws SQLException {
return convertAs(object, () -> new Timestamp(toMillis(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convertToTimestamp, convertToTime, convertToDate - same concerns as with the convertTo method: allocations, heavy Converters.getConverter. Most of this could be avoided for a good number of cases (probably, the only exception is the source column of the OBJECT type).

@SeriyBg SeriyBg requested a review from devozerov January 15, 2021 10:17
@SeriyBg SeriyBg merged commit e81cdbf into main Jan 19, 2021
@SeriyBg SeriyBg deleted the types-support branch January 19, 2021 09:44
@SeriyBg SeriyBg added this to the 4.2 milestone Apr 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants