Skip to content
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
59d344e
Pin polars >=1.38 and <1.40
Matt711 Apr 7, 2026
4b40d81
Pin polars >=1.38 and <1.40
Matt711 Apr 7, 2026
22524d6
merge conflict
Matt711 Apr 7, 2026
714fefe
remove unnecessary loop
Matt711 Apr 7, 2026
fa3c810
bump minimum version
Matt711 Apr 7, 2026
a84384d
add workaround for epty df with nonzero rows
Matt711 Apr 8, 2026
fbb4e57
Merge branch 'main' into imp/polars/bump-polars-1.40
Matt711 Apr 8, 2026
f9181e8
broaden version set
Matt711 Apr 13, 2026
3582005
merge conflict
Matt711 Apr 13, 2026
8892885
Bump dependnecy lower bounds to 1.35
mroeschke May 11, 2026
a6c4404
Remove 1.31 workarounds
mroeschke May 11, 2026
1143dbc
Remove 1.32 workarounds
mroeschke May 11, 2026
97be357
Remove 1.33 workarounds
mroeschke May 11, 2026
93470c7
Remove 1.34 workarounds
mroeschke May 11, 2026
7abf5b1
Remove 1.35 workarounds
mroeschke May 11, 2026
7defd7c
Update conda recipe
mroeschke May 11, 2026
2b7babc
Fix empty marker in test_rolling, fix log translation
mroeschke May 11, 2026
3ca8964
Merge remote-tracking branch 'upstream/main' into feat/cudf_polars/mi…
mroeschke May 11, 2026
e59bc97
Shorten timeout to 15
mroeschke May 11, 2026
f6c0dde
Merge branch 'feat/cudf_polars/min_bump' into imp/polars/bump-polars-…
mroeschke May 12, 2026
e823983
Merge remote-tracking branch 'upstream/main' into imp/polars/bump-pol…
mroeschke May 12, 2026
0b4b063
Merge remote-tracking branch 'upstream/main' into imp/polars/bump-pol…
mroeschke May 12, 2026
72009df
Reduce diff changes, support for 1.35
mroeschke May 12, 2026
1f72afd
Reduce diff more
mroeschke May 12, 2026
0ef9dcb
One more reduce diff
mroeschke May 12, 2026
94330c8
Merge remote-tracking branch 'upstream/main' into imp/polars/bump-pol…
mroeschke May 12, 2026
d3801f7
More fixes to mostly xpassing rolling tests in 1.39
mroeschke May 12, 2026
b13b15a
Merge remote-tracking branch 'upstream/main' into imp/polars/bump-pol…
mroeschke May 13, 2026
9c4762f
Pass engine to test_rolling_agg_first
mroeschke May 13, 2026
9ede121
Add decimal to float casting for is_bewteen (pass test_decimal_dynami…
mroeschke May 13, 2026
5b10326
Fix py-polars/tests/unit/sql/test_window_functions.py failures by car…
mroeschke May 13, 2026
d54780f
xfail, but prepare for HConcat passing strict to validate num_rows fo…
mroeschke May 13, 2026
4011aa4
Add fix for Polars test test_over_literal_cum_sum_26800
mroeschke May 13, 2026
3615f52
Skip test_scan_metrics
mroeschke May 13, 2026
a9161c5
Simplify EXPECTED_FAILURES
mroeschke May 13, 2026
3b36212
Merge remote-tracking branch 'upstream/main' into imp/polars/bump-pol…
mroeschke May 13, 2026
948b3a8
Revert "Add fix for Polars test test_over_literal_cum_sum_26800"
mroeschke May 14, 2026
387dd84
Revert "Fix py-polars/tests/unit/sql/test_window_functions.py failure…
mroeschke May 14, 2026
5b6471b
Skip Polars over test failures in 1.39
mroeschke May 14, 2026
7b50a4c
Replace test_concat_with_empty_dataframes_strict_25725 in expected fa…
mroeschke May 14, 2026
2250f60
Clarify test_scan_metrics skip reason
mroeschke May 14, 2026
a20a021
Merge remote-tracking branch 'upstream/release/26.06' into imp/polars…
mroeschke May 15, 2026
27c31a3
Also mark over test as failing for in-memory engine
mroeschke May 15, 2026
463d172
Add is_between test for new decimal/float cast
mroeschke May 15, 2026
13831f7
Update python/cudf_polars/cudf_polars/dsl/translate.py
mroeschke May 15, 2026
0d83811
Update python/cudf_polars/cudf_polars/dsl/translate.py
mroeschke May 15, 2026
f7e909e
remove whitespace
mroeschke May 15, 2026
63550e8
Update streaming fail list
mroeschke May 15, 2026
18d3ea9
Add coverage for cudf_polars/dsl/expressions/unary.py 206 4 …
mroeschke May 15, 2026
767735e
Add coverage for cudf_polars/dsl/ir.py 1123 3 …
mroeschke May 15, 2026
05d1946
Add coverage for cudf_polars/dsl/to_ast.py 87 1 …
mroeschke May 15, 2026
74b731e
Add no covers for the rest
mroeschke May 15, 2026
d2a018d
Merge remote-tracking branch 'upstream/release/26.06' into imp/polars…
mroeschke May 15, 2026
97d052b
Merge remote-tracking branch 'upstream/release/26.06' into imp/polars…
mroeschke May 15, 2026
445b5ac
Add pragma no cover continue in _is_remote_uri (because test_scan_ndj…
mroeschke May 15, 2026
0fb5680
Merge remote-tracking branch 'upstream/release/26.06' into imp/polars…
mroeschke May 18, 2026
66fdb71
skip test_hconcat_strict_different_heights for earlier Polars versions
mroeschke May 18, 2026
c0e23eb
Address coderabbit
mroeschke May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-129_arch-aarch64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ dependencies:
- packaging
- pandas>=2.0,<2.4.0
- pandoc
- polars>=1.35,<1.39
- polars>=1.35,<1.40
- pre-commit
- pyarrow>=19.0.0,<24
- pydata-sphinx-theme>=0.15.4
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-129_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ dependencies:
- packaging
- pandas>=2.0,<2.4.0
- pandoc
- polars>=1.35,<1.39
- polars>=1.35,<1.40
- pre-commit
- pyarrow>=19.0.0,<24
- pydata-sphinx-theme>=0.15.4
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-132_arch-aarch64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ dependencies:
- packaging
- pandas>=2.0,<2.4.0
- pandoc
- polars>=1.35,<1.39
- polars>=1.35,<1.40
- pre-commit
- pyarrow>=19.0.0,<24
- pydata-sphinx-theme>=0.15.4
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-132_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ dependencies:
- packaging
- pandas>=2.0,<2.4.0
- pandoc
- polars>=1.35,<1.39
- polars>=1.35,<1.40
- pre-commit
- pyarrow>=19.0.0,<24
- pydata-sphinx-theme>=0.15.4
Expand Down
2 changes: 1 addition & 1 deletion conda/recipes/cudf-polars/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ requirements:
- python
- pylibcudf =${{ version }}
- rapidsmpf =${{ minor_version }}
- polars>=1.35,<1.39
- polars>=1.35,<1.40
- packaging
- ${{ pin_compatible("cuda-version", upper_bound="x", lower_bound="x") }}
- if: cuda_major == "12"
Expand Down
2 changes: 1 addition & 1 deletion dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ dependencies:
# 'nvidia-ml-py' provides the 'pynvml' module
- &nvidia_ml_py nvidia-ml-py>=12
- packaging
- polars>=1.35,<1.39
- polars>=1.35,<1.40

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah we should cap this at 1.40 (even though 1.40.1 is out). I'll do the next bump in the next RAPIDS release.

- typing_extensions>=4.0.0
run_cudf_polars_dask:
common:
Expand Down
18 changes: 9 additions & 9 deletions python/cudf_polars/cudf_polars/dsl/expressions/rolling.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
offsets_to_windows,
range_window_bounds,
)
from cudf_polars.utils.versions import POLARS_VERSION_LT_136
from cudf_polars.utils.versions import POLARS_VERSION_LT_136, POLARS_VERSION_LT_139

if TYPE_CHECKING:
from collections.abc import Sequence
Expand Down Expand Up @@ -79,14 +79,14 @@ def to_request(
elif isinstance(value, expr.Agg):
child = value.children[0]
col = child.evaluate(df, context=ExecutionContext.ROLLING)
if POLARS_VERSION_LT_136 and value.name == "var": # pragma: no cover
if (POLARS_VERSION_LT_136 or not POLARS_VERSION_LT_139) and value.name == "var":
# Polars variance produces null if nvalues <= ddof
# libcudf produces NaN. However, we can get the polars
# behaviour by setting the minimum window size to ddof +
# 1.
#
# We still need this check, polars is not hitting it because
# of https://github.com/pola-rs/polars/pull/25117
# In polars 1.36-1.38 this code path is not hit because
# rolling goes through the IR path (not RollingWindow).
# See https://github.com/pola-rs/polars/pull/25117
min_periods = value.options + 1
else:
col = value.evaluate(
Expand Down Expand Up @@ -146,12 +146,12 @@ def __init__(
raise NotImplementedError(
"Incorrect handling of empty groups for list collection"
)
if POLARS_VERSION_LT_136 and not plc.rolling.is_valid_rolling_aggregation(
if (
POLARS_VERSION_LT_136 or not POLARS_VERSION_LT_139
) and not plc.rolling.is_valid_rolling_aggregation(
agg.dtype.plc_type, agg.agg_request
):
raise NotImplementedError(
f"Unsupported rolling aggregation {agg}"
) # pragma: no cover; polars may raise ahead of time
raise NotImplementedError(f"Unsupported rolling aggregation {agg}")

def do_evaluate( # noqa: D102
self, df: DataFrame, *, context: ExecutionContext = ExecutionContext.FRAME
Expand Down
13 changes: 6 additions & 7 deletions python/cudf_polars/cudf_polars/dsl/expressions/unary.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,14 @@ def do_evaluate(
if maintain_order:
column = column.sorted_like(values)
return column
elif self.name == "set_sorted": # pragma: no cover
# TODO: LazyFrame.set_sorted is proper IR concept (ie. FunctionIR::Hint)
# and is is currently not implemented. We should reimplement it as a MapFunction.
elif self.name == "set_sorted":

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

FYI @rjzamora Polars optimizer selectivity hint "set_sorted" is implemented in this PR

(column,) = (child.evaluate(df, context=context) for child in self.children)
(asc,) = self.options
if isinstance(self.options[0], str):
descending = self.options[0] == "descending" # pragma: no cover
else:
descending, _ = self.options
order = (
plc.types.Order.ASCENDING
if asc == "ascending"
else plc.types.Order.DESCENDING
plc.types.Order.DESCENDING if descending else plc.types.Order.ASCENDING
)
null_order = plc.types.NullOrder.BEFORE
if column.null_count > 0 and (n := column.size) > 1:
Expand Down
18 changes: 14 additions & 4 deletions python/cudf_polars/cudf_polars/dsl/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -3187,19 +3187,21 @@ def do_evaluate(
class HConcat(IR):
"""Concatenate dataframes horizontally."""

__slots__ = ("should_broadcast",)
_non_child = ("schema", "should_broadcast")
_n_non_child_args = 2
__slots__ = ("should_broadcast", "strict")
_non_child = ("schema", "should_broadcast", "strict")
_n_non_child_args = 3

def __init__(
self,
schema: Schema,
should_broadcast: bool, # noqa: FBT001
strict: bool, # noqa: FBT001
*children: IR,
):
self.schema = schema
self.should_broadcast = should_broadcast
self._non_child_args = (schema, should_broadcast)
self.strict = strict
self._non_child_args = (schema, should_broadcast, strict)
self.children = children

@staticmethod
Expand Down Expand Up @@ -3242,6 +3244,7 @@ def do_evaluate(
cls,
schema: Schema,
should_broadcast: bool, # noqa: FBT001
strict: bool, # noqa: FBT001
*dfs: DataFrame,
context: IRExecutionContext,
) -> DataFrame:
Expand All @@ -3259,6 +3262,13 @@ def do_evaluate(
result = DataFrame(ordered, stream=stream)
else:
max_rows = max(df.num_rows for df in dfs)
if strict and any(df.num_rows != max_rows for df in dfs):
heights = [df.num_rows for df in dfs]
msg = (
f"cannot concat DataFrames horizontally"
f" with strict=True: height mismatch {heights}"
)
raise pl.exceptions.ShapeError(msg)
# Horizontal concatenation extends shorter tables with nulls
result = DataFrame(
itertools.chain.from_iterable(
Expand Down
125 changes: 112 additions & 13 deletions python/cudf_polars/cudf_polars/dsl/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from cudf_polars.utils.versions import (
POLARS_VERSION_LT_136,
POLARS_VERSION_LT_138,
POLARS_VERSION_LT_139,
)

if TYPE_CHECKING:
Expand All @@ -45,6 +46,29 @@
__all__ = ["Translator", "translate_named_expr"]


def _align_decimal_float_for_comparison(
*operands: expr.Expr,
) -> tuple[expr.Expr, ...]:
"""
Cast Decimal operands to Float64 when mixed with Float operands.

libcudf does not produce correct results for Decimal vs Float
comparisons. This matches Polars' supertype resolution where
Decimal + Float yields Float64.
"""
has_decimal = any(plc.traits.is_fixed_point(op.dtype.plc_type) for op in operands)
has_float = any(plc.traits.is_floating_point(op.dtype.plc_type) for op in operands)
if has_decimal and has_float:
f64 = DataType(pl.Float64())
return tuple(
expr.Cast(f64, False, op) # noqa: FBT003
if plc.traits.is_fixed_point(op.dtype.plc_type)
else op
for op in operands
)
return operands


def _check_compression(data: bytes) -> str | None:
# Vendored from Polars' SupportedCompression::check in
# polars-io/src/utils/compression.rs
Expand Down Expand Up @@ -125,7 +149,7 @@ def translate_ir(self, *, n: int | None = None) -> ir.IR:
# IR is versioned with major.minor, minor is bumped for backwards
# compatible changes (e.g. adding new nodes), major is bumped for
# incompatible changes (e.g. renaming nodes).
if (version := self.visitor.version()) >= (12, 1):
if (version := self.visitor.version()) >= (12, 2):
e = NotImplementedError(
f"No support for polars IR {version=}"
) # pragma: no cover; no such version for now.
Expand Down Expand Up @@ -247,6 +271,17 @@ def __exit__(self, *args: Any) -> None:
self.translator._expr_context = self._prev


def _is_dynamic_pred(visitor: Any, expr_ir: Any) -> bool:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

FYI: dynamic_pred is another hint the optimizer injects into the plan when it sees a .sort("col").head(k) so an engine can skip rows that won't enter the top-k.

try:
visitor.view_expression(expr_ir.node)
except Exception as e:
if str(e) == "dynamic_pred":
return True
raise # pragma: no cover
else:
return False


@singledispatch
def _translate_ir(node: Any, translator: Translator, schema: Schema) -> ir.IR:
raise NotImplementedError(
Expand Down Expand Up @@ -307,7 +342,7 @@ def _(node: plrs._ir_nodes.Scan, translator: Translator, schema: Schema) -> ir.I
if typ in ("csv", "ndjson"):
for p in paths:
if plc.io.SourceInfo._is_remote_uri(p):
continue
continue # pragma: no cover
data = _read_file_bytes(Path(p))
compression = _check_compression(data)
if compression is not None:
Expand All @@ -326,9 +361,12 @@ def _(node: plrs._ir_nodes.Scan, translator: Translator, schema: Schema) -> ir.I
n_rows,
row_index,
include_file_paths,
translate_named_expr(translator, n=node.predicate, schema=schema)
if node.predicate is not None
else None,
(
None
if node.predicate is None
or _is_dynamic_pred(translator.visitor, node.predicate)
else translate_named_expr(translator, n=node.predicate, schema=schema)
),
parquet_options,
)

Expand Down Expand Up @@ -537,6 +575,8 @@ def _(node: plrs._ir_nodes.Slice, translator: Translator, schema: Schema) -> ir.
def _(node: plrs._ir_nodes.Filter, translator: Translator, schema: Schema) -> ir.IR:
with set_node(translator.visitor, node.input):
inp = translator.translate_ir(n=None)
if _is_dynamic_pred(translator.visitor, node.predicate):
return inp
mask = translate_named_expr(translator, n=node.predicate, schema=inp.schema)
return ir.Filter(schema, mask, inp)

Expand Down Expand Up @@ -585,9 +625,14 @@ def _(node: plrs._ir_nodes.Union, translator: Translator, schema: Schema) -> ir.

@_translate_ir.register
def _(node: plrs._ir_nodes.HConcat, translator: Translator, schema: Schema) -> ir.IR:
if POLARS_VERSION_LT_139:
strict = False # pragma: no cover
else:
_, strict, _ = node.options
return ir.HConcat(
schema,
False, # noqa: FBT003
strict,
*(translator.translate_ir(n=n) for n in node.inputs),
)

Expand Down Expand Up @@ -625,7 +670,7 @@ def _(node: plrs._ir_nodes.Sink, translator: Translator, schema: Schema) -> ir.I
if k in {"mkdir", "maintain_order", "sync_on_close"}:
options[k] = v

if sink_kind in ("Csv", "NDJson"):
if sink_kind in ("Csv", "NDJson", "Json"):
compression = format_options.get("compression")
if compression and compression != "Uncompressed":
raise NotImplementedError(
Expand Down Expand Up @@ -748,6 +793,10 @@ def _(
)
(closed,) = options
lop, rop = expr.BooleanFunction._BETWEEN_OPS[closed]
if not POLARS_VERSION_LT_139:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can you write a test in cudf_polars for this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure added in 463d172

# IsBetween type coercion does not
# insert casts for Decimal/Float comparisons.
column, lo, hi = _align_decimal_float_for_comparison(column, lo, hi)
return expr.BinOp(
dtype,
plc.binaryop.BinaryOperator.LOGICAL_AND,
Expand Down Expand Up @@ -820,14 +869,9 @@ def _(
dtype: DataType,
schema: Schema,
) -> expr.Expr:
if isinstance(
if hasattr(plrs._expr_nodes, "RollingGroupOptions") and isinstance(
node.options, plrs._expr_nodes.RollingGroupOptions
): # pragma: no cover; polars gives Aexpr::Rolling node now
# TODO: As of polars 1.36.0, rolling is represented as a Rolling expression node
# but is currently not implemented in the python node visitor. Once we support it,
# we should move this branch to separate translator dispatch function for the new node.

# pl.col("a").rolling(...)
): # pragma: no cover; polars >=1.36 uses AExpr::Rolling now
with set_expr_context(translator, ExecutionContext.ROLLING):
agg = translator.translate_expr(n=node.function, schema=schema)
name_generator = unique_names(schema)
Expand Down Expand Up @@ -928,6 +972,61 @@ def _(
assert_never(node.options)


def _translate_rolling(
node: plrs._expr_nodes.Rolling,
translator: Translator,
dtype: DataType,
schema: Schema,
) -> expr.Expr:
# pl.col("a").rolling(...)
with set_expr_context(translator, ExecutionContext.ROLLING):
Comment thread
mroeschke marked this conversation as resolved.
Resolved
agg_expr = translator.translate_expr(n=node.function, schema=schema)
orderby = translator.visitor.view_expression(node.index_column).name
orderby_dtype = schema[orderby].plc_type
if plc.traits.is_integral(orderby_dtype):
orderby_dtype = plc.DataType(plc.TypeId.INT64)
name_generator = unique_names(schema)
aggs, named_post_agg = decompose_single_agg(
expr.NamedExpr(next(name_generator), agg_expr),
name_generator,
is_top=True,
context=ExecutionContext.ROLLING,
)
named_aggs = [a for a, _ in aggs]
closed_window = node.closed_window
if isinstance(named_post_agg.value, expr.Col):
(named_agg,) = named_aggs
return expr.RollingWindow(
named_agg.value.dtype,
orderby_dtype,
node.offset,
node.period,
closed_window,
orderby,
named_agg.value,
)
replacements: dict[expr.Expr, expr.Expr] = {
expr.Col(a.value.dtype, a.name): expr.RollingWindow(
a.value.dtype,
orderby_dtype,
node.offset,
node.period,
closed_window,
orderby,
a.value,
)
for a in named_aggs
}
return replace([named_post_agg.value], replacements)[0]


# Rolling was added in polars 1.39. Use explicit registration (not annotation-based)
# so that Python 3.14's stricter get_type_hints() resolution does not fail when
# running on older polars versions that lack this node type.
if hasattr(plrs._expr_nodes, "Rolling"):
_translate_expr.register(plrs._expr_nodes.Rolling)(_translate_rolling)


@_translate_expr.register
def _(
node: plrs._expr_nodes.Literal,
Expand Down
1 change: 1 addition & 0 deletions python/cudf_polars/cudf_polars/experimental/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def _decompose(
input_ir = HConcat(
schema,
True, # noqa: FBT003
False, # noqa: FBT003
*unique_input_irs,
)
partition_info[input_ir] = PartitionInfo(count=partition_count)
Expand Down
2 changes: 1 addition & 1 deletion python/cudf_polars/cudf_polars/experimental/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ def _sink_to_file(
# Path.open returns IO[Any] but SinkInfo needs more specific IO types
sink = plc.io.types.SinkInfo([f]) # type: ignore[arg-type]
Sink._write_csv(sink, use_options, df)
elif kind == "Json" if POLARS_VERSION_LT_137 else "NDJson":
elif kind == ("Json" if POLARS_VERSION_LT_137 else "NDJson"):
mode = "wb" if writer_state is None else "ab"
with Path.open(Path(path), mode) as f:
# Path.open returns IO[Any] but SinkInfo needs more specific IO types
Expand Down
Loading
Loading