aerospike / aerospike-client-java

Aerospike Java Client Library
Other
236 stars 212 forks source link

getDouble(String) throws ClassCastException if record in table is int #68

Closed SnoopInf closed 7 years ago

SnoopInf commented 7 years ago

Sometimes there is a need to have Double values mixed with Integer values in Aerospike. E.g. some job inserts double values, but when it comes to 0 or other integer it simply inserts 0 instead of 0.0 or 5 instead of 5.0. But in Aerospike client it's not convenient to try-cast value to any type. Java is OK to convert int to double, so there should be no problem to do that in Aerospike driver.

Steps to reproduce:

  1. Insert some integer value to any bin (e.g. named "value") in Aerospike. E.g. 0 or 5.
  2. Try to get this value via record.getDouble("value").

Expected:

  1. There's a value converted to double, e.g. 0.0 or 5.0

Actual:

  1. java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
      at com.aerospike.client.Record.getDouble(Record.java:77)
      ....

Short test to reproduce:

    @Test
    public void testGetDoubleWhenIntInserted() {
        Map<String, Object> vals = new HashMap<>();
        vals.put("value", 5);
        Record record = new Record(vals, 1, 1);
        record.getDouble("value");
    }

Aerospike client version: 3.3.0

BrianNichols commented 7 years ago

Java supports automatic int/long conversion to double, but this is not true for their object counterparts (Integer,Long to Double).

Double d1 = (Integer)5;  // does not compile
Double d2 = (Double)(Integer)5;  // does not compile
double d3 = (Integer)5; // works by converting Integer to native int which automatically converts to native double.

Since the record contains objects (not native types), you will need to check the type of the object and convert yourself.

Map<String, Object> vals = new HashMap<>();
vals.put("value", 5);
Record record = new Record(vals, 1, 1);
Object obj = record.getValue("value");
double dbl = 0.0;

if (obj instanceof Long) {
    dbl = (Long)obj;
}
else if (obj instanceof Integer) {
    dbl = (Integer)obj;
}
else {
    dbl = (Double)obj;
}
System.out.println(dbl);

Another alternative is to always write the bin as double:

int val = 5;
client.put(null, key, new Bin("bin", (double)val));
SnoopInf commented 7 years ago

Right, that's exactly what i have to do now. Solution with writing always double values is something i initially requested from DMP team, but they don't use java driver and for some reason solution which they use inserts integers even if you cast them to double.

@Test
    public void testGetDoubleWhenIntInserted() {
        Map<String, Object> vals = new HashMap<>();
        vals.put("value", 5);
        Record record = new Record(vals, 1, 1);
        Object o = record.getValue("value");
        double dbl = 0.0;
        if (o instanceof Integer) {
            dbl = (Integer)o;
        } else if (o instanceof Long) {
            dbl = (Long)o;
        } else {
            dbl = (Double)o;
        }
        System.out.println(dbl);
    }
}

We could also use getDouble() after integer check, since double values are anyways cast to long inside the driver

public double getDouble(String name) {
        Object result = this.getValue(name);
        return result instanceof Double?((Double)result).doubleValue():(result != null?Double.longBitsToDouble(((Long)result).longValue()):0.0D);
    }

I just wonder if it's possible to do type check for Integer here right near type check for double? Something like this:

public double getDouble(String name) {
        Object result = this.getValue(name);
        return result instanceof Double ? (Double) result : 
                result instanceof Integer ? ((Integer)(result)).doubleValue() : 
                        (result != null?Double.longBitsToDouble((Long) result):0.0D);

    }
BrianNichols commented 7 years ago

The server only stores/returns 8 byte integers for integer bins, so it's not necessary to check for Integer. Only need to check for Double/Long.

The problem is that the java client used to store doubles in a long type (which happens to also need 8 bytes of storage) when the server did not support a double type. The longBitsToDouble() conversions works for this case, but this conversion does not work when the bits are a real long value. The function for that case is:

public double getDouble(String name) {
    Object result = this.getValue(name);
    return (result instanceof Double)? (Double)result : (result != null)? (Long)result : 0.0;      
}

We are concerned that removing longBitsToDouble() would break code for databases that still have double bits stored as long bits. The change to store doubles as new double type was only made 9 months ago, and we would like to keep this functionality intact for at least a year.

In the meantime, you can add this function to another class and then call that.