Skip to content

feat: close upstream coverage gaps for DataFusion 55.1.0 - #1763

Open
timsaucer wants to merge 16 commits into
apache:mainfrom
timsaucer:feat/upstream-coverage-gaps
Open

timsaucer wants to merge 16 commits into
apache:mainfrom
timsaucer:feat/upstream-coverage-gaps

Conversation

@timsaucer

@timsaucer timsaucer commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

No issue filed. The gaps were found by auditing the Python API against upstream DataFusion 55.1.0 with the check-upstream skill. Gaps that already have open issues (#1571–#1575, #1577, #1668, #1669) are left out.

Rationale for this change

Several upstream functions, optional arguments, and DataFrame methods were not reachable from Python. The audit also turned up bugs where options on aggregate and window functions were silently dropped.

What changes are included in this PR?

  • New functions: array math and array_first, any_value, file metadata functions, and a batch of Spark functions and pyspark aliases.
  • Optional arguments upstream already supports, such as distinct= on several aggregates, characters= on the trim functions, null_treatment= on lead/lag, and new DataFrame.explain options.
  • DataFrame.fill_nan.
  • FFI: ScalarUDF and WindowUDF accept a bare PyCapsule, and the capsule overloads now type-check correctly.
  • More Pythonic range and gen_series: they accept plain ints, and range(stop) works like Python's built-in.
  • Fixes: chaining builder methods or .over() onto an aggregate or window function no longer discards options already set on it (ordering, null treatment, explicit window frame). mean(x, filter=...) no longer raises TypeError.

Every new function has a doctest and pytest coverage. The same options are still dropped when .over() converts an aggregate to a window function; that is tracked separately in #1764.

Are there any user-facing changes?

Yes, new functions, arguments, and methods. Two breaking changes are documented in docs/source/user-guide/upgrade-guides.md:

  • distinct is inserted before filter in bit_and, bit_or, mean, percentile_cont, quantile_cont, and string_agg, matching sum and avg in 54.0.0. Pass filter by keyword.
  • spark.last_day's parameter is renamed from col to date to match pyspark.

Code that chained builder methods or .over() onto a configured function may see different results; it was previously getting silently wrong ones.

🤖 Generated with Claude Code

timsaucer and others added 6 commits September 25, 2026 08:19
…nctions

Close gaps found by auditing the Python API against upstream DataFusion
55.1.0.

- Add the any_value aggregate.
- Add array_add, array_subtract, array_scale, array_sum, array_avg,
  array_product, and array_first, each with its list_* alias.
- Add Spark monthname, weekday, atan2, hypot, pow/power, quote, and
  concat_ws. concat_ws calls the UDF directly so *cols stays variadic.
- Fix the aggregations guide, which listed regr_slope twice and omitted
  regr_sxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t kwargs

- Add rand and substring_index as aliases of random and substr_index.
- Add input_file_name and file_row_index, which report the source file
  and row offset during a file scan.
- Add a distinct argument to bit_and, bit_or, mean, percentile_cont,
  quantile_cont, and string_agg. distinct goes before filter, matching
  sum and avg; the upgrade guide covers positional callers.
- Fix mean, which passed filter into avg's distinct slot and raised a
  TypeError whenever filter was given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add getbit, dateadd, datediff, datepart, sha, ceiling, printf,
  char_length, and character_length as aliases of their Spark primaries.
- Add substr, whose len argument is optional as in pyspark. It calls the
  UDF directly because upstream expr_fn::substring always takes a length.
- Rename the spark.last_day parameter from col to date to match pyspark,
  with an upgrade-guide note for keyword callers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tring, substr

- btrim, ltrim, rtrim, and trim take an optional characters argument
  naming the set to strip.
- array_to_string and its aliases take an optional null_string that is
  written in place of NULL elements.
- substr takes an optional length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace NaN in floating-point columns, optionally limited to a subset.
Mirrors fill_null and wraps upstream DataFrame::fill_nan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IGNORE_NULLS skips null values when counting shift_offset rows, matching
SQL LEAD/LAG ... IGNORE NULLS. The argument is appended after order_by,
so existing calls are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timsaucer
timsaucer force-pushed the feat/upstream-coverage-gaps branch from 5f33af0 to 7cf8fb5 Compare September 25, 2026 13:22
timsaucer and others added 5 commits September 25, 2026 09:44
…xplain

Expose the remaining upstream ExplainOption fields as keywords on
DataFrame.explain. Each defaults to None, which falls back to the
matching datafusion.explain.* session setting, so existing calls are
unaffected. New ExplainAnalyzeLevel and ExplainMetricCategory enums sit
beside ExplainFormat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream ExprFunctionExt methods on an Expr start from an empty builder,
so build() resets every option not set again. The Python function
wrappers already apply their keyword options, so calls such as
string_agg(..., order_by=...).distinct().build() silently dropped the
ordering, and .filter() dropped order_by, and so on.

Seed the builder from the expression's existing params instead. A window
frame equal to the default for its order_by is left unset so build()
derives it again from the final order_by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AggregateUDF already accepted the capsule returned by
__datafusion_aggregate_udf__ as well as an object exposing it (apache#1277).
Extend the same to ScalarUDF and WindowUDF, with matching overloads on
udf and udwf, so the three UDF kinds import the same way. Add FFI
example tests for all three, including the previously untested
AggregateUDF path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ABC is defined in datafusion.catalog beside CatalogProvider and
SchemaProvider, which are in its __all__, but was only listed in the
package root's __all__.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
str(df) is the Pythonic way to get a string and already uses __repr__
and the configurable formatter, so check-upstream should not flag the
upstream to_string as a gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timsaucer
timsaucer force-pushed the feat/upstream-coverage-gaps branch from 7cf8fb5 to 5d43576 Compare September 25, 2026 13:45
timsaucer and others added 5 commits September 25, 2026 10:53
user_defined.py imported CapsuleType from _typeshed, which does not
define it. Pyright reports `"CapsuleType" is unknown import symbol`, so
_PyCapsule resolved to Unknown and every overload and parameter typed
with it accepted any argument. This affected the udaf/from_pycapsule
hints added in apache#1277 as well as the new udf/udwf ones.

Import it from types on Python 3.13+ and from typing_extensions (already
a dependency below 3.13) otherwise. Checked with pyright at
--pythonversion 3.10 and 3.13: udf/udaf/udwf and each from_pycapsule
accept a CapsuleType and return the right wrapper, and
WindowUDF.from_pycapsule(1) is now rejected where it previously passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A built window function always stores a concrete frame, so the builder
could not tell a frame the user chose from the default. A frame equal to
the no-order_by default was treated as unset and re-derived as the
running frame once order_by was chained, silently changing results.

Record on the Python Expr whether over() or window_frame() set the frame
explicitly, and pass that to builder_from_expr so the frame is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defaulting to NullTreatment.RESPECT_NULLS passed Some(RespectNulls) to
Rust, which adds "RESPECT NULLS" to the generated column name and breaks
code that refers to an un-aliased lead/lag output by name. Default to
None instead; respecting nulls is already the behavior when unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calling over() on an expression that was already a window function
rebuilt it from an empty builder, silently dropping its order_by,
null_treatment, and explicit window frame. For example,
lead(v, order_by="i", null_treatment=IGNORE_NULLS).over(Window(partition_by=[g]))
lost both the ordering and IGNORE NULLS.

Start from builder_from_expr instead, and apply only the options the
Window sets. Pass the Python-side explicit-frame flag through so a frame
equal to the default is not re-derived when over() adds an order_by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
range() required all three of start, stop, and step as Expr, although
upstream also accepts range(stop) and range(start, stop). Make stop and
step optional in the bindings for both range and gen_series, so a single
argument is the upper bound starting at 0, like Python's built-in range.

Accept plain ints for start, stop, and step, coerced to literals, so
callers no longer need lit(). Passing step without stop raises ValueError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timsaucer
timsaucer marked this pull request as ready for review September 25, 2026 16:30
@timsaucer
timsaucer requested a balanced review from Copilot September 25, 2026 16:30
@timsaucer timsaucer changed the title feat: close upstream coverage gaps found by audit against DataFusion 55.1.0 feat: close upstream coverage gaps for DataFusion 55.1.0 Sep 25, 2026

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Public explain enums need consistent exports, and identified documentation and distinct-option coverage gaps remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 2 Low severity

Open (3)
What changed in this PR

Expands Python coverage for DataFusion 55.1.0 APIs and fixes option preservation across expression builders.

Changes:

  • Adds array, aggregate, metadata, Spark, range, and DataFrame APIs.
  • Preserves aggregate/window builder options and supports bare FFI capsules.
  • Adds documentation, migration guidance, and Python integration tests.
File Description
.ai/​skills/​check-upstream/​SKILL.md Records to_string audit guidance.
crates/​core/​src/​dataframe.rs Binds explain options and fill_nan.
crates/​core/​src/​expr.rs Preserves expression-builder options.
crates/​core/​src/​functions.rs Binds new functions and arguments.
crates/​core/​src/​spark_functions.rs Adds Spark function bindings.
crates/​core/​src/​udf.rs Imports bare scalar UDF capsules.
crates/​core/​src/​udwf.rs Imports bare window UDF capsules.
docs/​source/​user-guide/​common-operations/​aggregations.md Updates aggregate catalog.
docs/​source/​user-guide/​upgrade-guides.md Documents breaking signature changes.
examples/​datafusion-ffi-example/​python/​tests/​_test_aggregate_udf.py Tests bare aggregate capsules.
examples/​datafusion-ffi-example/​python/​tests/​_test_scalar_udf.py Tests bare scalar capsules.
examples/​datafusion-ffi-example/​python/​tests/​_test_window_udf.py Tests bare window capsules.
python/​datafusion/​catalog.py Exports TableProviderFactory.
python/​datafusion/​dataframe.py Exposes explain options and fill_nan.
python/​datafusion/​expr.py Tracks explicit window frames.
python/​datafusion/​functions/​__init__.py Adds functions and optional arguments.
python/​datafusion/​functions/​spark.py Adds Spark functions and aliases.
python/​datafusion/​user_defined.py Accepts bare UDF capsules.
python/​tests/​test_aggregation.py Covers aggregate additions.
python/​tests/​test_dataframe.py Covers DataFrame and window options.
python/​tests/​test_expr.py Covers builder option preservation.
python/​tests/​test_functions.py Covers scalar and array additions.
python/​tests/​test_lambda.py Covers array_first.
python/​tests/​test_spark_functions.py Covers Spark additions and aliases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

"""Graphviz DOT format for graph rendering."""


class ExplainAnalyzeLevel(Enum):
Returns NULL if every value in the group is NULL. Which value is returned
is not specified and may differ between runs.

If using the builder functions described in ref:`_aggregation` this function ignores
Comment on lines +367 to +368
("bit_and_distinct", f.bit_and(column("b"), distinct=True), [4]),
("bit_or_distinct", f.bit_or(column("b"), distinct=True), [6]),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants