Repository navigation
fix: floor // on DuckDB for negative operands and floats - #3994
Conversation
DuckDB's `//` truncates integers toward zero and is true division on floats. Step the truncated integer quotient down by one when the remainder is nonzero and the signs differ, and floor the float quotient, as Polars does. Integer types are kept and no intermediate value can overflow. Before: -7 // 2 = -3, -2**63 // 3 = -3074457345618258602, 7.0 // 2.0 = 3.5. After: -4, -3074457345618258603, 3.0.
FBruzzesi
left a comment
There was a problem hiding this comment.
Thanks for the PR @ReguiguiMohamed, and for the clear before/after table!
The approach is right: correcting the truncated quotient keeps everything in integer arithmetic.
A few things before we can merge:
test_floordiv_large_integersfails on old polars: before 0.20.7 computes integer//through float64, and the minimum-versions and pretty-old-versions CI jobs pin 0.20.4. I checked: 0.20.4 fails both assertions and 0.20.8 passes. Suggestion inline.- Nothing tests the
typeofcheck: if you delete it and apply the integer rule to floats, every test still passes. Only inputs like-1.0 // 0.1tell the two rules apart: the quotient is exactly-10.0but the remainder is nonzero, so the integer rule gives-11. That case also shows the xfail should be keyed on where Python and Polars actually disagree, not on the divisor. Suggestion inline. - Put the two rules in their own CASE arms: DuckDB resolves
typeofwhen it binds the query and drops CASE arms it can prove dead. In the flatint_fix | (q < t)there is no arm to drop, so integer rows also evaluate the float comparison. On 20M rows (sum(a // b), best of 5, DuckDB 1.5.6), integers go from 32.8 ms to 24.1 ms. Floats stay the same (15.3 vs 14.8 ms) and both versions give identical results. Suggestion inline, together with more descriptive names.
Happy to push these to your branch if that's easier, just let me know.
Checked, no change needed
- Unsigned integers: the signs never differ, so the step-down never runs and cannot underflow.
- Nulls,
infandNaNpass through unchanged. - DECIMAL: DuckDB already returns DOUBLE for DECIMAL
//, so it takes the float arm.
Not for this PR
Pre-existing, for follow-up issues:
- DuckDB raises
Overflow in divisionfor-2**63 // -1. Polars returns-2**63. x // 0.0returns null on DuckDB because of the zero guard in_sql/expr.py, which every SQL backend shares. Polars returnsinf,-inforNaN.- Ibis, SQLFrame and PySpark lose precision past 2**53, as you noted in the description.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
Thanks for the careful review, and for checking all those cases. I applied all four suggestions in 6169bd1, fixing a small typo in the last one ( I'm happy to open the follow-up issues for the overflow on |
FBruzzesi
left a comment
There was a problem hiding this comment.
Thanks for applying everything @ReguiguiMohamed! This looks good to me 🙌🏼
I'm happy to open the follow-up issues for the overflow on -2**63 // -1 and the x // 0.0 null if you'd like them.
Yes please to the two follow-up issues. They're out of scope here: neither is about rounding direction, and both affect more than DuckDB. Here is what I see on main, in case it helps:
- Float floor division by zero: every backend except Polars returns null for [1.0, -1.0, 0.0] // 0.0: pandas, pandas[nullable], pandas[pyarrow], pyarrow, Dask, DuckDB, SQLFrame and Ibis. Polars computes floor(a / b) with IEEE division, so it returns inf, -inf and NaN, and only returns null for integers. Since the other backends agree with each other, I'd open this as a question rather than a bug: should float // 0.0 follow Polars, or stay null?
division_by_zero_test.pyonly covers the integer column, so nothing pins either behaviour yet. -2**63 // -1: Polars, pandas, pandas[nullable] and Dask wrap around to -2**63. pyarrow, pandas[pyarrow] and DuckDB raise an overflow error, Ibis raises a conversion error, and SQLFrame returns 9.223372036854776e+18. The SQLFrame result is the most urgent of these, because it's silently wrong rather than an error.
Please include the repro and the per-backend results in each issue.
|
Thanks again @FBruzzesi for the review! Opened #4070 and #4071 for the two follow-ups. |

Description
DuckDB's
//truncates ints toward zero and on floats it's true division, so-7 // 2gave -3 and7.0 // 2.0gave 3.5. Now the int quotient steps down by one when the remainder is nonzero and the signs differ, and float quotients get floored like Polars.-7 // 2-2**63 // 37.0 // 2.0-7.0 // 2.0Unsigned types stay unsigned and nothing overflows (
10 // (2**63-1)is 0).Floats follow Polars and PyArrow, so
1.0 // 0.1is 10. pandas follows Python and gives 9, so that case is xfailed for pandas. The large-int test xfails ibis, sqlframe and pyspark since they floor through float64. I left that for a separate issue.What type of PR is this? (check all applicable)
Related issues
AI assistance
Checklist
pytestpassing locally (not CI):Screenshot
Summary by CodeRabbit