Repository navigation
[CALCITE-7827] Document precision loss in DECIMAL/REAL comparisons and how to widen them to DOUBLE - #5297
[CALCITE-7827] Document precision loss in DECIMAL/REAL comparisons and how to widen them to DOUBLE#5297sbroeder wants to merge 7 commits into
Conversation
…narrowed commonTypeForBinaryComparison() picked whichever operand was approximate as the common type for a comparison against DECIMAL, narrowing the DECIMAL side to that operand's precision. A 32-bit REAL/FLOAT holds only ~7 significant digits, so a DECIMAL literal or column with more digits than that can silently collide with a different value after narrowing. For example, 59999943 and 59999945 both round to the same float. By widening the common type to DOUBLE whenever the exact-numeric operand is a DECIMAL, we don't lose precision. Updated ArrowAdapterTest's expected plan, which now casts a FLOAT column to DOUBLE when compared against a DECIMAL literal.
…ercion override Updated the design based on input from the JIRA so that users could opt in to thei behavior and avoid breaking changes for existing users. Extract the decision into a new protected AbstractTypeCoercion#approximateExactComparisonType(...) hook. A system that needs the precision-safe behavior can opt in with a small TypeCoercionFactory overriding this one method, the same pattern already used by TypeCoercionImpl and demonstrated by SqlToRelConverterTest#testNaturalJoinCastNoCoercion. Reworked the tests accordingly.
|
This looks like a reasonable approach, I will review this |
vlsi
left a comment
There was a problem hiding this comment.
Have you considered the extension point that already exists? TypeCoercion#commonTypeForBinaryComparison is public, so a subclass of TypeCoercionImpl can let super pick the type and replace an approximate result with DOUBLE when one operand is DECIMAL:
class WideningTypeCoercion extends TypeCoercionImpl {
WideningTypeCoercion(RelDataTypeFactory typeFactory, SqlValidator validator) {
super(typeFactory, validator);
}
@Override public @Nullable RelDataType commonTypeForBinaryComparison(
@Nullable RelDataType type1, @Nullable RelDataType type2) {
final RelDataType type = super.commonTypeForBinaryComparison(type1, type2);
if (type != null && SqlTypeUtil.isApproximateNumeric(type)
&& (SqlTypeUtil.isDecimal(type1) || SqlTypeUtil.isDecimal(type2))) {
return factory.createTypeWithNullability(
factory.createSqlType(SqlTypeName.DOUBLE), type.isNullable());
}
return type;
}
}It is installed with SqlValidator.Config#withTypeCoercionFactory(WideningTypeCoercion::new), the same way JdbcTest and SqlToRelConverterTest in this PR install theirs. AbstractTypeCoercion calls commonTypeForBinaryComparison again for the element types of ARRAY and MAP and for the fields of ROW, so the override covers nested types too, just as the new hook does. I compiled this class and compared its results with the hook's for DECIMAL, REAL, FLOAT, and INTEGER operands, nullable and not, and nested in ARRAY, MAP, and ROW; they agree. This also follows Mihai's suggestion in JIRA to replace the type coercion class.
As far as I can tell, approximateExactComparisonType saves the user a couple of lines, and Calcite would have to keep it as a protected API from then on. If you agree, the PR could document the existing override instead of adding a hook, and the tests that exercise the hook could be rewritten against the override or dropped.
A few points apply whichever extension point we document.
Precision of DOUBLE. The description says that widening to DOUBLE means "we don't lose precision". A DOUBLE carries 53 bits of mantissa, a little under 16 decimal digits, so DECIMAL values with more significant digits still collide: 123456789012345.678 and 123456789012345.679 convert to the same double. The tests declare DECIMAL(18, 3), which holds 18 significant digits, so values of the tests' own column type can collide. The overrides in the tests also widen only DECIMAL, while INTEGER compared with REAL has the same defect: 16777216 and 16777217 are the same float. The JIRA comment says INTEGER and BIGINT never carry more precision than a REAL holds, but REAL has 24 bits of mantissa, INTEGER has 31, and BIGINT has 63, more than even DOUBLE's 53. The javadoc line "Override to widen to DOUBLE for DECIMAL" recommends this as the fix, so I would either drop it or state the precisions for which DOUBLE is exact.
Opting in from JDBC. No connection property selects a TypeCoercionFactory. A jdbc:calcite: application can opt in only by subclassing CalcitePrepareImpl and overriding CalcitePreparingStmt#createSqlValidator, then passing the result to Driver#withPrepareFactory. JdbcTest in this PR uses Hook.STRING_TO_QUERY instead, which is meant for testing. Do you expect JDBC users to take that route, or is the opt-in meant only for applications that build their own validator through Frameworks or SqlValidatorUtil?
Documentation and release notes. Nothing outside the javadoc mentions the opt-in. Someone who hits wrong join results will look in the "Implicit Type Conversion" section of site/_docs/reference.md, so a short paragraph there, with the override above, would reach them. The PR title and description still describe changing the default, including an ArrowAdapterTest update that is no longer in the diff. The title becomes the line in the release notes, so it should say what the PR now adds.
Which statements the override changes. The comparison common type is used by =, <, and the other binary comparisons, by BETWEEN (folded left to right over the operands), by IN and quantified comparisons (TypeCoercionImpl#inOperationCoercion and #quantifyOperationCoercion), by NATURAL JOIN and JOIN USING in SqlToRelConverter, and for the elements of ARRAY, MAP, and ROW. It does not apply to CASE, COALESCE, set operations (UNION, INTERSECT, EXCEPT), or multi-row VALUES, which go through getWiderTypeFor and leastRestrictive. leastRestrictive already returns DOUBLE for DECIMAL with REAL, so by default CASE WHEN b THEN d ELSE r END has type DOUBLE while d = r compares as REAL, and the override brings comparisons in line with the rest. Whatever documentation we add should name both lists, so that a user knows which statements change.
|
I don't see any objections in @vlsi's review to merging this. |
|
To be explicit: I would rather not merge this as is. Three points from my review are objections, not suggestions:
If we keep the hook, I would ask at least for 2 and 3 to be fixed, and for the javadoc to say which statements the hook affects. If you think the PR should go in anyway, the tests need work too:
|
|
Yes, please change the issue and commit to reflect the fact that you just make the type coercion user-configurable in more ways. |
Drop approximateExactComparisonType() and instead use commonTypeForBinaryComparison override. Document use in the TypeCoercion javadoc and site/_docs/reference.md Rewrite the tests against a shared WideningTypeCoercion.
|
@vlsi Thank you for your review and suggestions. I agree it would be better to not add another API unnecessarily. I have adopted your suggestion, added tests to pin current behavior and demonstrate the widening behavior, and documentation. |
|
Thanks, 75be51b resolves the first two objections from my comment: the new API is gone, and the javadoc says where DOUBLE stops being exact. It also pins the default in
Smaller points:
|
|
@vlsi Thank you for the review. I believe I have addressed your comments. |
vlsi
left a comment
There was a problem hiding this comment.
Thanks, this looks good to me. One thing I would fix before merge is in the inline comment on reference.md.
Optional nits:
- The narrowing rule in the
AbstractTypeCoercionjavadoc applies to FLOAT and DOUBLE as well, not only REAL. - The operator lists could also name
IS [NOT] DISTINCT FROM, which goes throughbinaryComparisonCoerciontoo.
| subclass and install it via | ||
| `SqlValidator.Config#withTypeCoercionFactory`. This opt-in is available | ||
| to applications using `Frameworks` or `SqlValidatorUtil`; there is no | ||
| JDBC connection property for it. |
There was a problem hiding this comment.
DOUBLE holds about 15 significant digits, and the paragraph no longer says so: the caveat went away with the TypeCoercion javadoc I asked you to trim. Suggested:
| JDBC connection property for it. | |
| JDBC connection property for it. `DOUBLE` has a 53-bit mantissa, about | |
| 15 decimal digits, so `DECIMAL` values with more significant digits can | |
| still compare as equal after widening. |
|
I will let @vlsi merge this when he thinks it's ready. |
|
@vlsi Thanks again for the review. I adopted all your suggestions. |
|



When commonTypeForBinaryComparison picks a common type for a DECIMAL-vs-approximate-numeric comparison, it returns the approximate type as the common type, and then the coercion layer narrows the exact operand to match it. A 32-bit REAL holds only about 7 significant digits, so distinct DECIMAL values can silently compare as equal — for example, 59999943.000 and 59999945.000 both round to the same REAL.
This change documents the issue and leaves the default behavior unchanged. It also provides an opt-in path for applications that need correct comparisons.
Jira Link
CALCITE-7827