Skip to content

Commit 8027fcb

Browse files
timsaucerclaude
andcommitted
feat: add rand, substring_index, file metadata functions, and distinct 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>
1 parent 5a0e79d commit 8027fcb

5 files changed

Lines changed: 222 additions & 14 deletions

File tree

‎crates/core/src/functions.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@ expr_fn_vec!(arrow_metadata);
622622
expr_fn_vec!(with_metadata);
623623
expr_fn!(union_tag, arg1);
624624
expr_fn!(random);
625+
expr_fn!(input_file_name);
626+
expr_fn!(file_row_index);
625627

626628
#[pyfunction]
627629
fn get_field(expr: PyExpr, names: Vec<PyExpr>) -> PyExpr {
@@ -768,16 +770,17 @@ pub fn approx_percentile_cont_with_weight(
768770
}
769771

770772
#[pyfunction]
771-
#[pyo3(signature = (sort_expression, percentile, filter=None))]
773+
#[pyo3(signature = (sort_expression, percentile, distinct=None, filter=None))]
772774
pub fn percentile_cont(
773775
sort_expression: PySortExpr,
774776
percentile: f64,
777+
distinct: Option<bool>,
775778
filter: Option<PyExpr>,
776779
) -> PyDataFusionResult<PyExpr> {
777780
let agg_fn =
778781
functions_aggregate::expr_fn::percentile_cont(sort_expression.sort, lit(percentile));
779782

780-
add_builder_fns_to_aggregate(agg_fn, None, filter, None, None)
783+
add_builder_fns_to_aggregate(agg_fn, distinct, filter, None, None)
781784
}
782785

783786
// We handle last_value explicitly because the signature expects an order_by
@@ -1070,6 +1073,8 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
10701073
m.add_wrapped(wrap_pyfunction!(power))?;
10711074
m.add_wrapped(wrap_pyfunction!(radians))?;
10721075
m.add_wrapped(wrap_pyfunction!(random))?;
1076+
m.add_wrapped(wrap_pyfunction!(input_file_name))?;
1077+
m.add_wrapped(wrap_pyfunction!(file_row_index))?;
10731078
m.add_wrapped(wrap_pyfunction!(regexp_count))?;
10741079
m.add_wrapped(wrap_pyfunction!(regexp_instr))?;
10751080
m.add_wrapped(wrap_pyfunction!(regexp_like))?;

‎docs/source/user-guide/upgrade-guides.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,26 @@ ctx.execute(plan, partitions=0) # before
198198
ctx.execute(plan, partition=0) # after
199199
```
200200

201+
### More aggregate functions accept `distinct`
202+
203+
{py:func}`~datafusion.functions.bit_and`,
204+
{py:func}`~datafusion.functions.bit_or`,
205+
{py:func}`~datafusion.functions.mean`,
206+
{py:func}`~datafusion.functions.percentile_cont`,
207+
{py:func}`~datafusion.functions.quantile_cont`, and
208+
{py:func}`~datafusion.functions.string_agg` now accept a `distinct` argument.
209+
As with `sum` and `avg` in 54.0.0, `distinct` is inserted *before* `filter`, so
210+
code that passed `filter` (or, for `string_agg`, `order_by`) positionally must
211+
pass it by keyword.
212+
213+
```python
214+
f.bit_and(column("a"), my_filter) # before
215+
f.bit_and(column("a"), filter=my_filter) # after
216+
```
217+
218+
Passing `filter` to `mean` previously raised a `TypeError`, whether passed
219+
positionally or by keyword; it now works.
220+
201221
### Changes to the `datafusion-python-util` crate
202222

203223
Extension libraries written in Rust usually depend on the

‎python/datafusion/functions/__init__.py‎

Lines changed: 121 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def _warn_if_expr_for_literal_arg(
209209
"exp",
210210
"extract",
211211
"factorial",
212+
"file_row_index",
212213
"find_in_set",
213214
"first_value",
214215
"flatten",
@@ -224,6 +225,7 @@ def _warn_if_expr_for_literal_arg(
224225
"in_list",
225226
"initcap",
226227
"inner_product",
228+
"input_file_name",
227229
"instr",
228230
"is_nan",
229231
"isnan",
@@ -334,6 +336,7 @@ def _warn_if_expr_for_literal_arg(
334336
"power",
335337
"quantile_cont",
336338
"radians",
339+
"rand",
337340
"random",
338341
"range",
339342
"rank",
@@ -382,6 +385,7 @@ def _warn_if_expr_for_literal_arg(
382385
"substr",
383386
"substr_index",
384387
"substring",
388+
"substring_index",
385389
"sum",
386390
"tan",
387391
"tanh",
@@ -2342,6 +2346,15 @@ def substr_index(string: Expr, delimiter: Expr | str, count: Expr | int) -> Expr
23422346
return Expr(f.substr_index(string.expr, delimiter.expr, count.expr))
23432347

23442348

2349+
def substring_index(string: Expr, delimiter: Expr | str, count: Expr | int) -> Expr:
2350+
"""Returns an indexed substring.
2351+
2352+
See Also:
2353+
This is an alias for :py:func:`substr_index`.
2354+
"""
2355+
return substr_index(string, delimiter, count)
2356+
2357+
23452358
def substring(string: Expr, position: Expr | int, length: Expr | int) -> Expr:
23462359
"""Substring from the ``position`` with ``length`` characters.
23472360
@@ -3487,6 +3500,64 @@ def random() -> Expr:
34873500
return Expr(f.random())
34883501

34893502

3503+
def rand() -> Expr:
3504+
"""Returns a random value in the range ``0.0 <= x < 1.0``.
3505+
3506+
See Also:
3507+
This is an alias for :py:func:`random`.
3508+
"""
3509+
return random()
3510+
3511+
3512+
def input_file_name() -> Expr:
3513+
"""Returns the path of the file that produced the current row.
3514+
3515+
Only valid inside a scan of a file-backed table; evaluating it anywhere
3516+
else raises an error.
3517+
3518+
Examples:
3519+
>>> import tempfile, os
3520+
>>> import pyarrow as pa, pyarrow.parquet as pq
3521+
>>> tmp = tempfile.mkdtemp()
3522+
>>> path = os.path.join(tmp, "data.parquet")
3523+
>>> pq.write_table(pa.table({"a": [1, 2]}), path)
3524+
>>> ctx = dfn.SessionContext()
3525+
>>> df = ctx.read_parquet(path)
3526+
>>> result = df.select(dfn.functions.input_file_name().alias("f"))
3527+
>>> result.collect_column("f")[0].as_py().endswith("data.parquet")
3528+
True
3529+
3530+
See Also:
3531+
:py:func:`file_row_index`.
3532+
"""
3533+
return Expr(f.input_file_name())
3534+
3535+
3536+
def file_row_index() -> Expr:
3537+
"""Returns the zero-based position of the current row within its source file.
3538+
3539+
The index restarts at zero for each file, so rows from different files in one
3540+
scan can share a value. Only valid inside a scan of a Parquet table;
3541+
evaluating it anywhere else raises an error.
3542+
3543+
Examples:
3544+
>>> import tempfile, os
3545+
>>> import pyarrow as pa, pyarrow.parquet as pq
3546+
>>> tmp = tempfile.mkdtemp()
3547+
>>> path = os.path.join(tmp, "data.parquet")
3548+
>>> pq.write_table(pa.table({"a": [10, 20, 30]}), path)
3549+
>>> ctx = dfn.SessionContext()
3550+
>>> df = ctx.read_parquet(path).filter(dfn.col("a") > dfn.lit(10))
3551+
>>> result = df.select(dfn.functions.file_row_index().alias("i"))
3552+
>>> result.collect_column("i").to_pylist()
3553+
[1, 2]
3554+
3555+
See Also:
3556+
:py:func:`input_file_name`.
3557+
"""
3558+
return Expr(f.file_row_index())
3559+
3560+
34903561
def array_append(array: Expr, element: Expr) -> Expr:
34913562
"""Appends an element to the end of an array.
34923563
@@ -5335,6 +5406,7 @@ def approx_percentile_cont_with_weight(
53355406
def percentile_cont(
53365407
sort_expression: Expr | SortExpr,
53375408
percentile: float,
5409+
distinct: bool = False,
53385410
filter: Expr | None = None,
53395411
) -> Expr:
53405412
"""Computes the exact percentile of input values using continuous interpolation.
@@ -5343,11 +5415,12 @@ def percentile_cont(
53435415
percentile value rather than an approximation.
53445416
53455417
If using the builder functions described in ref:`_aggregation` this function ignores
5346-
the options ``order_by``, ``null_treatment``, and ``distinct``.
5418+
the options ``order_by`` and ``null_treatment``.
53475419
53485420
Args:
53495421
sort_expression: Values for which to find the percentile
53505422
percentile: This must be between 0.0 and 1.0, inclusive
5423+
distinct: If True, duplicate values are removed before computing
53515424
filter: If provided, only compute against rows for which the filter is True
53525425
53535426
Examples:
@@ -5367,23 +5440,38 @@ def percentile_cont(
53675440
... ).alias("v")])
53685441
>>> result.collect_column("v")[0].as_py()
53695442
3.5
5443+
5444+
>>> df = ctx.from_pydict({"a": [1.0, 1.0, 1.0, 4.0]})
5445+
>>> result = df.aggregate(
5446+
... [], [dfn.functions.percentile_cont(
5447+
... dfn.col("a"), 0.5, distinct=True,
5448+
... ).alias("v")])
5449+
>>> result.collect_column("v")[0].as_py()
5450+
2.5
53705451
"""
53715452
sort_expr_raw = sort_or_default(sort_expression)
53725453
filter_raw = filter.expr if filter is not None else None
5373-
return Expr(f.percentile_cont(sort_expr_raw, percentile, filter=filter_raw))
5454+
return Expr(
5455+
f.percentile_cont(
5456+
sort_expr_raw, percentile, distinct=distinct, filter=filter_raw
5457+
)
5458+
)
53745459

53755460

53765461
def quantile_cont(
53775462
sort_expression: Expr | SortExpr,
53785463
percentile: float,
5464+
distinct: bool = False,
53795465
filter: Expr | None = None,
53805466
) -> Expr:
53815467
"""Computes the exact percentile of input values using continuous interpolation.
53825468
53835469
See Also:
53845470
This is an alias for :py:func:`percentile_cont`.
53855471
"""
5386-
return percentile_cont(sort_expression, percentile, filter)
5472+
return percentile_cont(
5473+
sort_expression, percentile, distinct=distinct, filter=filter
5474+
)
53875475

53885476

53895477
def array_agg(
@@ -5750,13 +5838,17 @@ def max(expression: Expr, filter: Expr | None = None) -> Expr:
57505838
return Expr(f.max(expression.expr, filter=filter_raw))
57515839

57525840

5753-
def mean(expression: Expr, filter: Expr | None = None) -> Expr:
5841+
def mean(
5842+
expression: Expr,
5843+
distinct: bool = False,
5844+
filter: Expr | None = None,
5845+
) -> Expr:
57545846
"""Returns the average (mean) value of the argument.
57555847
57565848
See Also:
57575849
This is an alias for :py:func:`avg`.
57585850
"""
5759-
return avg(expression, filter)
5851+
return avg(expression, distinct=distinct, filter=filter)
57605852

57615853

57625854
def median(
@@ -6621,16 +6713,19 @@ def any_value(expression: Expr, filter: Expr | None = None) -> Expr:
66216713
return Expr(f.any_value(expression.expr, filter=filter_raw))
66226714

66236715

6624-
def bit_and(expression: Expr, filter: Expr | None = None) -> Expr:
6716+
def bit_and(
6717+
expression: Expr, distinct: bool = False, filter: Expr | None = None
6718+
) -> Expr:
66256719
"""Computes the bitwise AND of the argument.
66266720
66276721
This aggregate function will bitwise compare every value in the input partition.
66286722
66296723
If using the builder functions described in ref:`_aggregation` this function ignores
6630-
the options ``order_by``, ``null_treatment``, and ``distinct``.
6724+
the options ``order_by`` and ``null_treatment``.
66316725
66326726
Args:
66336727
expression: Argument to perform bitwise calculation on
6728+
distinct: If True, evaluate each unique value of expression only once
66346729
filter: If provided, only compute against rows for which the filter is True
66356730
66366731
Examples:
@@ -6653,19 +6748,22 @@ def bit_and(expression: Expr, filter: Expr | None = None) -> Expr:
66536748
5
66546749
"""
66556750
filter_raw = filter.expr if filter is not None else None
6656-
return Expr(f.bit_and(expression.expr, filter=filter_raw))
6751+
return Expr(f.bit_and(expression.expr, distinct=distinct, filter=filter_raw))
66576752

66586753

6659-
def bit_or(expression: Expr, filter: Expr | None = None) -> Expr:
6754+
def bit_or(
6755+
expression: Expr, distinct: bool = False, filter: Expr | None = None
6756+
) -> Expr:
66606757
"""Computes the bitwise OR of the argument.
66616758
66626759
This aggregate function will bitwise compare every value in the input partition.
66636760
66646761
If using the builder functions described in ref:`_aggregation` this function ignores
6665-
the options ``order_by``, ``null_treatment``, and ``distinct``.
6762+
the options ``order_by`` and ``null_treatment``.
66666763
66676764
Args:
66686765
expression: Argument to perform bitwise calculation on
6766+
distinct: If True, evaluate each unique value of expression only once
66696767
filter: If provided, only compute against rows for which the filter is True
66706768
66716769
Examples:
@@ -6690,7 +6788,7 @@ def bit_or(expression: Expr, filter: Expr | None = None) -> Expr:
66906788
6
66916789
"""
66926790
filter_raw = filter.expr if filter is not None else None
6693-
return Expr(f.bit_or(expression.expr, filter=filter_raw))
6791+
return Expr(f.bit_or(expression.expr, distinct=distinct, filter=filter_raw))
66946792

66956793

66966794
def bit_xor(
@@ -7315,6 +7413,7 @@ def ntile(
73157413
def string_agg(
73167414
expression: Expr,
73177415
delimiter: str,
7416+
distinct: bool = False,
73187417
filter: Expr | None = None,
73197418
order_by: list[SortKey] | SortKey | None = None,
73207419
) -> Expr:
@@ -7325,11 +7424,12 @@ def string_agg(
73257424
their string equivalents.
73267425
73277426
If using the builder functions described in ref:`_aggregation` this function ignores
7328-
the options ``distinct`` and ``null_treatment``.
7427+
the option ``null_treatment``.
73297428
73307429
Args:
73317430
expression: Argument to perform bitwise calculation on
73327431
delimiter: Text to place between each value of expression
7432+
distinct: If True, each unique value of expression is included only once
73337433
filter: If provided, only compute against rows for which the filter is True
73347434
order_by: Set the ordering of the expression to evaluate. Accepts
73357435
column names or expressions.
@@ -7352,6 +7452,14 @@ def string_agg(
73527452
... ).alias("s")])
73537453
>>> result.collect_column("s")[0].as_py()
73547454
'y,z'
7455+
7456+
>>> df = ctx.from_pydict({"a": ["y", "x", "y"]})
7457+
>>> result = df.aggregate(
7458+
... [], [dfn.functions.string_agg(
7459+
... dfn.col("a"), ",", distinct=True, order_by="a",
7460+
... ).alias("s")])
7461+
>>> result.collect_column("s")[0].as_py()
7462+
'x,y'
73557463
"""
73567464
order_by_raw = sort_list_to_raw_sort_list(order_by)
73577465
filter_raw = filter.expr if filter is not None else None
@@ -7360,6 +7468,7 @@ def string_agg(
73607468
f.string_agg(
73617469
expression.expr,
73627470
delimiter,
7471+
distinct=distinct,
73637472
filter=filter_raw,
73647473
order_by=order_by_raw,
73657474
)

0 commit comments

Comments
 (0)