Skip to content

GH-1406: Read Float4 values as double and BigDecimal with correct precision - #1407

Open
scruz-denodo wants to merge 1 commit into
apache:mainfrom
scruz-denodo:float4-precision
Open

scruz-denodo wants to merge 1 commit into
apache:mainfrom
scruz-denodo:float4-precision

Conversation

@scruz-denodo

Copy link
Copy Markdown

What's Changed

ArrowFlightJdbcFloat4VectorAccessor no longer exposes float-widening artifacts when a Float4Vector is read as a double or a decimal.

  • getDouble() (and therefore getObject(Double.class)) now returns Double.parseDouble(Float.toString(value)) instead of (double) value. A 0.1f cell now gives 0.1, not 0.10000000149011612.
  • getBigDecimal() and getBigDecimal(int scale) now use new BigDecimal(Float.toString(value)) instead of BigDecimal.valueOf(value), which widened the float to a double first.
  • NaN and ±Infinity are still returned by getDouble(). getBigDecimal() still throws SQLException for them, and null cells still return 0.0 / null.

Tests (ArrowFlightJdbcFloat4VectorAccessorTest):

  • getDouble and getBigDecimal expectations now use Float.toString.
  • A new test checks that (float) getDouble() == getFloat().
  • A new test covers 0.1f, -0.1f, 0.2f, 0.3f, 1.1f, 123.456f, 3.4e-5f and -0.0f through getDouble, getObject(Double.class), getBigDecimal() and getBigDecimal(9).
  • A 0.1f test checks 0.1, 0.1 and 0.100000000.
  • A NaN getDouble test is added.

Closes #1406.

ArrowFlightJdbcFloat4VectorAccessor widened the float to double in getDouble() and passed it to BigDecimal.valueOf(double) in getBigDecimal(), which exposed the binary approximation of the float: 0.1f was returned as 0.10000000149011612. Convert through the float's decimal representation instead, so getDouble(), getObject(Double.class) and getBigDecimal() return the value the float stands for.
@github-actions

Copy link
Copy Markdown

Thank you for opening a pull request!

Please label the PR with one or more of:

  • bug-fix
  • chore
  • dependencies
  • documentation
  • enhancement

Also, add the 'breaking-change' label if appropriate.

See CONTRIBUTING.md for details.

@lidavidm lidavidm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The issue states:

For a finite float, getDouble(), getBigDecimal() and getObject(Double.class) should return the decimal value the float represents. That is the shortest decimal that uniquely identifies the float, the same one Float.toString yields. For 0.1f that is 0.1, which is what getString() already returns. Other JDBC drivers show this behaviour for REAL columns.

Where is it documented that this is expected? What other JDBC drivers show this behavior? IMO, converting float to double via the string representation is very unexpected.

@scruz-denodo

Copy link
Copy Markdown
Author

Hi,
the root cause is with how Java does the cast from the float. In that process is where the precision of the initial number is lost.

Here is a testing class showing the root cause and results returned by Postgresq, SQLServer, Oracle, Gizmo (for testing a flight-sql database) and Duckdb.

package scruz.test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class FloatTests {

    public static void main(String args[]) throws Exception {
        TestConfig[] testConfigs = new TestConfig[]{
            new TestConfig("jdbc:postgresql://localhost:5432/vdp", "org.postgresql.Driver", "xxxx", "xxxx", "SELECT CAST(0.1 AS real)"),
            new TestConfig("jdbc:sqlserver://<host>:1433;databaseName=master;encrypt=false", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "xxxx", "xxxx", "SELECT CAST(0.1 AS real);"),
            new TestConfig("jdbc:oracle:thin:@<host>:1521/ORCLPDB1", "oracle.jdbc.OracleDriver", "xxxx", "xxxx", "SELECT CAST(0.1 AS real) FROM dual"),
            new TestConfig("jdbc:arrow-flight-sql://localhost:31337?useEncryption=0&disableCertificateVerification=true", "org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver", "admin", "admin", "SELECT CAST(0.1 AS real)"),
            new TestConfig("jdbc:duckdb:", "org.duckdb.DuckDBDriver", "", "", "SELECT CAST(0.1 AS real)"),
        };

        for (TestConfig testConfig : testConfigs) {
            Class.forName(testConfig.DRIVER);
            System.out.println("\nTesting " + testConfig.URI);
            try (Connection connection = DriverManager.getConnection(testConfig.URI, testConfig.USER, testConfig.PASSWORD);
                 PreparedStatement pst = connection.prepareStatement(testConfig.QUERY);
                 ResultSet rs = pst.executeQuery()) {
                rs.next();
                System.out.println("getString() " + rs.getString(1));
                System.out.println("getObject() " + rs.getObject(1));
                System.out.println("getFloat() " + rs.getFloat(1));
                System.out.println("getDouble() " + rs.getDouble(1));
                System.out.println("getBigDecimal() " + rs.getBigDecimal(1));
            }
        }

        //Java isolated test
        System.out.println("\nJava isolated");
        final float value = 0.1f;
        System.out.println("value: " + value);
        System.out.println("value (with cast double): " + (double)value);
        System.out.println("value (with double + toString): " + Double.parseDouble(Float.toString(value)));
    }

    private static class TestConfig {
        final String URI;
        final String DRIVER;
        final String USER;
        final String PASSWORD;
        final String QUERY;

        TestConfig(String URI, String DRIVER, String USER, String PASSWORD, String QUERY) {
            this.URI = URI;
            this.DRIVER = DRIVER;
            this.USER = USER;
            this.PASSWORD = PASSWORD;
            this.QUERY = QUERY;
        }
    }
}

The output shows that:

  • Oracle, and Postgresql do not have problems with the conversion.
  • SQLServer and Duckdb have problem with the getDouble method returning 0.10000000149011612
  • Gizmo through flight-sql JDBC driver have problem with getObject, getDouble and getBigDecimal

The isolated java test shows the root cause and the proposed solution.

This is the entire output.

Testing jdbc:postgresql://localhost:5432/vdp
getString() 0.1
getObject() 0.1
getFloat() 0.1
getDouble() 0.1
getBigDecimal() 0.1

Testing jdbc:sqlserver://<host>:1433;databaseName=master;encrypt=false
getString() 0.1
getObject() 0.1
getFloat() 0.1
getDouble() 0.10000000149011612
getBigDecimal() 0.1

Testing jdbc:oracle:thin:@<host>:1521/ORCLPDB1
getString() 0.1
getObject() 0.1
getFloat() 0.1
getDouble() 0.1
getBigDecimal() 0.1

Testing jdbc:arrow-flight-sql://localhost:31337?useEncryption=0&disableCertificateVerification=true
getString() 0.1
getObject() 0.10000000149011612
getFloat() 0.1
getDouble() 0.10000000149011612
getBigDecimal() 0.10000000149011612

Testing jdbc:duckdb:
getString() 0.1
getObject() 0.1
getFloat() 0.1
getDouble() 0.10000000149011612
getBigDecimal() 0.1

Java isolated
value: 0.1
value (with cast double): 0.10000000149011612
value (with double + toString): 0.1

About the proposed solution, we are already using that solution for avoiding the issue on other sites for a long time.
I was searching for alternatives but I do not find any other fix which can deal with the 100% of scenarios.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Flight SQL JDBC] Read Float4 values as double and BigDecimal with correct precision

2 participants