Add missing array functions (#1468)
* Add missing array/list functions and aliases (#1452)
Add new array functions from upstream DataFusion v53: array_any_value,
array_distance, array_max, array_min, array_reverse, arrays_zip,
string_to_array, and gen_series. Add corresponding list_* aliases and
missing list_* aliases for existing functions (list_empty, list_pop_back,
list_pop_front, list_has, list_has_all, list_has_any). Also add
array_contains/list_contains as aliases for array_has, generate_series
as alias for gen_series, and string_to_list as alias for string_to_array.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add unit tests for new array/list functions and aliases
Tests cover all functions and aliases added in the previous commit:
array_any_value, array_distance, array_max, array_min, array_reverse,
arrays_zip, string_to_array, gen_series, generate_series,
array_contains, list_contains, list_empty, list_pop_back,
list_pop_front, list_has, list_has_all, list_has_any, and list_*
aliases for the new functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Improve array function APIs: optional params, better naming, restore comment
- Make null_string optional in string_to_array/string_to_list
- Make step optional in gen_series/generate_series
- Rename second_array to element in array_contains/list_has/list_contains
- Restore # Window Functions section comment in __all__
- Add tests for optional parameter variants
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Consolidate array/list function tests using pytest parametrize
Reduce 26 individual tests to 14 test functions with parametrized
cases, eliminating boilerplate while maintaining full coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move list alias tests into existing test_array_functions parametrize block
Merge standalone tests for list_empty, list_pop_back, list_pop_front,
list_has, array_contains, list_contains, list_has_all, and list_has_any
into the existing parametrized test_array_functions block alongside
their array_* counterparts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Merge test_array_any_value into parametrized test_any_value_aliases
Use the richer multi-row dataset (including all-nulls case) for both
array_any_value and list_any_value via the parametrized test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add arrays_overlap and list_overlap as aliases for array_has_any
These aliases match the upstream DataFusion SQL-level aliases, completing
the set of missing array functions from issue #1452.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add docstring examples for optional params in string_to_array and gen_series
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update AGENTS file to demonstrate preferred method of documenting python functions
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/AGENTS.md b/AGENTS.md
index 1853a84..f6fdfbd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,3 +25,20 @@
- `SKILL.md` — The skill definition with YAML frontmatter (name, description, argument-hint) and detailed instructions.
- Additional supporting files as needed.
+
+## Python Function Docstrings
+
+Every Python function must include a docstring with usage examples.
+
+- **Examples are required**: Each function needs at least one doctest-style example
+ demonstrating basic usage.
+- **Optional parameters**: If a function has optional parameters, include separate
+ examples that show usage both without and with the optional arguments. Pass
+ optional arguments using their keyword name (e.g., `step=dfn.lit(3)`) so readers
+ can immediately see which parameter is being demonstrated.
+- **Reuse input data**: Use the same input data across examples wherever possible.
+ The examples should demonstrate how different optional arguments change the output
+ for the same input, making the effect of each option easy to understand.
+- **Alias functions**: Functions that are simple aliases (e.g., `list_sort` aliasing
+ `array_sort`) only need a one-line description and a `See Also` reference to the
+ primary function. They do not need their own examples.
diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs
index 5e61b71..8bb9277 100644
--- a/crates/core/src/functions.rs
+++ b/crates/core/src/functions.rs
@@ -94,6 +94,50 @@
}
#[pyfunction]
+fn array_distance(array1: PyExpr, array2: PyExpr) -> PyExpr {
+ let args = vec![array1.into(), array2.into()];
+ Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf(
+ datafusion::functions_nested::distance::array_distance_udf(),
+ args,
+ ))
+ .into()
+}
+
+#[pyfunction]
+fn arrays_zip(exprs: Vec<PyExpr>) -> PyExpr {
+ let exprs = exprs.into_iter().map(|x| x.into()).collect();
+ datafusion::functions_nested::expr_fn::arrays_zip(exprs).into()
+}
+
+#[pyfunction]
+#[pyo3(signature = (string, delimiter, null_string=None))]
+fn string_to_array(string: PyExpr, delimiter: PyExpr, null_string: Option<PyExpr>) -> PyExpr {
+ let mut args = vec![string.into(), delimiter.into()];
+ if let Some(null_string) = null_string {
+ args.push(null_string.into());
+ }
+ Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf(
+ datafusion::functions_nested::string::string_to_array_udf(),
+ args,
+ ))
+ .into()
+}
+
+#[pyfunction]
+#[pyo3(signature = (start, stop, step=None))]
+fn gen_series(start: PyExpr, stop: PyExpr, step: Option<PyExpr>) -> PyExpr {
+ let mut args = vec![start.into(), stop.into()];
+ if let Some(step) = step {
+ args.push(step.into());
+ }
+ Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf(
+ datafusion::functions_nested::range::gen_series_udf(),
+ args,
+ ))
+ .into()
+}
+
+#[pyfunction]
fn make_map(keys: Vec<PyExpr>, values: Vec<PyExpr>) -> PyExpr {
let keys = keys.into_iter().map(|x| x.into()).collect();
let values = values.into_iter().map(|x| x.into()).collect();
@@ -681,6 +725,10 @@
array_fn!(array_union, array1 array2);
array_fn!(array_except, first_array second_array);
array_fn!(array_resize, array size value);
+array_fn!(array_any_value, array);
+array_fn!(array_max, array);
+array_fn!(array_min, array);
+array_fn!(array_reverse, array);
array_fn!(cardinality, array);
array_fn!(flatten, array);
array_fn!(range, start stop step);
@@ -1152,6 +1200,14 @@
m.add_wrapped(wrap_pyfunction!(array_replace_all))?;
m.add_wrapped(wrap_pyfunction!(array_sort))?;
m.add_wrapped(wrap_pyfunction!(array_slice))?;
+ m.add_wrapped(wrap_pyfunction!(array_any_value))?;
+ m.add_wrapped(wrap_pyfunction!(array_distance))?;
+ m.add_wrapped(wrap_pyfunction!(array_max))?;
+ m.add_wrapped(wrap_pyfunction!(array_min))?;
+ m.add_wrapped(wrap_pyfunction!(array_reverse))?;
+ m.add_wrapped(wrap_pyfunction!(arrays_zip))?;
+ m.add_wrapped(wrap_pyfunction!(string_to_array))?;
+ m.add_wrapped(wrap_pyfunction!(gen_series))?;
m.add_wrapped(wrap_pyfunction!(flatten))?;
m.add_wrapped(wrap_pyfunction!(cardinality))?;
diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py
index 3febb44..1b26773 100644
--- a/python/datafusion/functions.py
+++ b/python/datafusion/functions.py
@@ -53,10 +53,13 @@
"approx_percentile_cont_with_weight",
"array",
"array_agg",
+ "array_any_value",
"array_append",
"array_cat",
"array_concat",
+ "array_contains",
"array_dims",
+ "array_distance",
"array_distinct",
"array_element",
"array_empty",
@@ -69,6 +72,8 @@
"array_intersect",
"array_join",
"array_length",
+ "array_max",
+ "array_min",
"array_ndims",
"array_pop_back",
"array_pop_front",
@@ -85,10 +90,13 @@
"array_replace_all",
"array_replace_n",
"array_resize",
+ "array_reverse",
"array_slice",
"array_sort",
"array_to_string",
"array_union",
+ "arrays_overlap",
+ "arrays_zip",
"arrow_cast",
"arrow_typeof",
"ascii",
@@ -153,6 +161,8 @@
"floor",
"from_unixtime",
"gcd",
+ "gen_series",
+ "generate_series",
"greatest",
"ifnull",
"in_list",
@@ -167,19 +177,31 @@
"left",
"length",
"levenshtein",
+ "list_any_value",
"list_append",
"list_cat",
"list_concat",
+ "list_contains",
"list_dims",
+ "list_distance",
"list_distinct",
"list_element",
+ "list_empty",
"list_except",
"list_extract",
+ "list_has",
+ "list_has_all",
+ "list_has_any",
"list_indexof",
"list_intersect",
"list_join",
"list_length",
+ "list_max",
+ "list_min",
"list_ndims",
+ "list_overlap",
+ "list_pop_back",
+ "list_pop_front",
"list_position",
"list_positions",
"list_prepend",
@@ -193,10 +215,12 @@
"list_replace_all",
"list_replace_n",
"list_resize",
+ "list_reverse",
"list_slice",
"list_sort",
"list_to_string",
"list_union",
+ "list_zip",
"ln",
"log",
"log2",
@@ -273,6 +297,8 @@
"stddev_pop",
"stddev_samp",
"string_agg",
+ "string_to_array",
+ "string_to_list",
"strpos",
"struct",
"substr",
@@ -2794,6 +2820,15 @@
return Expr(f.array_empty(array.expr))
+def list_empty(array: Expr) -> Expr:
+ """Returns a boolean indicating whether the array is empty.
+
+ See Also:
+ This is an alias for :py:func:`array_empty`.
+ """
+ return array_empty(array)
+
+
def array_extract(array: Expr, n: Expr) -> Expr:
"""Extracts the element with the index n from the array.
@@ -2891,6 +2926,69 @@
return Expr(f.array_has_any(first_array.expr, second_array.expr))
+def array_contains(array: Expr, element: Expr) -> Expr:
+ """Returns true if the element appears in the array, otherwise false.
+
+ See Also:
+ This is an alias for :py:func:`array_has`.
+ """
+ return array_has(array, element)
+
+
+def list_has(array: Expr, element: Expr) -> Expr:
+ """Returns true if the element appears in the array, otherwise false.
+
+ See Also:
+ This is an alias for :py:func:`array_has`.
+ """
+ return array_has(array, element)
+
+
+def list_has_all(first_array: Expr, second_array: Expr) -> Expr:
+ """Determines if there is complete overlap ``second_array`` in ``first_array``.
+
+ See Also:
+ This is an alias for :py:func:`array_has_all`.
+ """
+ return array_has_all(first_array, second_array)
+
+
+def list_has_any(first_array: Expr, second_array: Expr) -> Expr:
+ """Determine if there is an overlap between ``first_array`` and ``second_array``.
+
+ See Also:
+ This is an alias for :py:func:`array_has_any`.
+ """
+ return array_has_any(first_array, second_array)
+
+
+def arrays_overlap(first_array: Expr, second_array: Expr) -> Expr:
+ """Returns true if any element appears in both arrays.
+
+ See Also:
+ This is an alias for :py:func:`array_has_any`.
+ """
+ return array_has_any(first_array, second_array)
+
+
+def list_overlap(first_array: Expr, second_array: Expr) -> Expr:
+ """Returns true if any element appears in both arrays.
+
+ See Also:
+ This is an alias for :py:func:`array_has_any`.
+ """
+ return array_has_any(first_array, second_array)
+
+
+def list_contains(array: Expr, element: Expr) -> Expr:
+ """Returns true if the element appears in the array, otherwise false.
+
+ See Also:
+ This is an alias for :py:func:`array_has`.
+ """
+ return array_has(array, element)
+
+
def array_position(array: Expr, element: Expr, index: int | None = 1) -> Expr:
"""Return the position of the first occurrence of ``element`` in ``array``.
@@ -3058,6 +3156,24 @@
return Expr(f.array_pop_front(array.expr))
+def list_pop_back(array: Expr) -> Expr:
+ """Returns the array without the last element.
+
+ See Also:
+ This is an alias for :py:func:`array_pop_back`.
+ """
+ return array_pop_back(array)
+
+
+def list_pop_front(array: Expr) -> Expr:
+ """Returns the array without the first element.
+
+ See Also:
+ This is an alias for :py:func:`array_pop_front`.
+ """
+ return array_pop_front(array)
+
+
def array_remove(array: Expr, element: Expr) -> Expr:
"""Removes the first element from the array equal to the given value.
@@ -3429,6 +3545,227 @@
return array_resize(array, size, value)
+def array_any_value(array: Expr) -> Expr:
+ """Returns the first non-null element in the array.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[None, 2, 3]]})
+ >>> result = df.select(
+ ... dfn.functions.array_any_value(dfn.col("a")).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ 2
+ """
+ return Expr(f.array_any_value(array.expr))
+
+
+def list_any_value(array: Expr) -> Expr:
+ """Returns the first non-null element in the array.
+
+ See Also:
+ This is an alias for :py:func:`array_any_value`.
+ """
+ return array_any_value(array)
+
+
+def array_distance(array1: Expr, array2: Expr) -> Expr:
+ """Returns the Euclidean distance between two numeric arrays.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[1.0, 2.0]], "b": [[1.0, 4.0]]})
+ >>> result = df.select(
+ ... dfn.functions.array_distance(
+ ... dfn.col("a"), dfn.col("b"),
+ ... ).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ 2.0
+ """
+ return Expr(f.array_distance(array1.expr, array2.expr))
+
+
+def list_distance(array1: Expr, array2: Expr) -> Expr:
+ """Returns the Euclidean distance between two numeric arrays.
+
+ See Also:
+ This is an alias for :py:func:`array_distance`.
+ """
+ return array_distance(array1, array2)
+
+
+def array_max(array: Expr) -> Expr:
+ """Returns the maximum value in the array.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[1, 2, 3]]})
+ >>> result = df.select(
+ ... dfn.functions.array_max(dfn.col("a")).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ 3
+ """
+ return Expr(f.array_max(array.expr))
+
+
+def list_max(array: Expr) -> Expr:
+ """Returns the maximum value in the array.
+
+ See Also:
+ This is an alias for :py:func:`array_max`.
+ """
+ return array_max(array)
+
+
+def array_min(array: Expr) -> Expr:
+ """Returns the minimum value in the array.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[1, 2, 3]]})
+ >>> result = df.select(
+ ... dfn.functions.array_min(dfn.col("a")).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ 1
+ """
+ return Expr(f.array_min(array.expr))
+
+
+def list_min(array: Expr) -> Expr:
+ """Returns the minimum value in the array.
+
+ See Also:
+ This is an alias for :py:func:`array_min`.
+ """
+ return array_min(array)
+
+
+def array_reverse(array: Expr) -> Expr:
+ """Reverses the order of elements in the array.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[1, 2, 3]]})
+ >>> result = df.select(
+ ... dfn.functions.array_reverse(dfn.col("a")).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ [3, 2, 1]
+ """
+ return Expr(f.array_reverse(array.expr))
+
+
+def list_reverse(array: Expr) -> Expr:
+ """Reverses the order of elements in the array.
+
+ See Also:
+ This is an alias for :py:func:`array_reverse`.
+ """
+ return array_reverse(array)
+
+
+def arrays_zip(*arrays: Expr) -> Expr:
+ """Combines multiple arrays into a single array of structs.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]})
+ >>> result = df.select(
+ ... dfn.functions.arrays_zip(dfn.col("a"), dfn.col("b")).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ [{'c0': 1, 'c1': 3}, {'c0': 2, 'c1': 4}]
+ """
+ args = [a.expr for a in arrays]
+ return Expr(f.arrays_zip(args))
+
+
+def list_zip(*arrays: Expr) -> Expr:
+ """Combines multiple arrays into a single array of structs.
+
+ See Also:
+ This is an alias for :py:func:`arrays_zip`.
+ """
+ return arrays_zip(*arrays)
+
+
+def string_to_array(
+ string: Expr, delimiter: Expr, null_string: Expr | None = None
+) -> Expr:
+ """Splits a string based on a delimiter and returns an array of parts.
+
+ Any parts matching the optional ``null_string`` will be replaced with ``NULL``.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": ["hello,world"]})
+ >>> result = df.select(
+ ... dfn.functions.string_to_array(
+ ... dfn.col("a"), dfn.lit(","),
+ ... ).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ ['hello', 'world']
+
+ Replace parts matching a ``null_string`` with ``NULL``:
+
+ >>> result = df.select(
+ ... dfn.functions.string_to_array(
+ ... dfn.col("a"), dfn.lit(","), null_string=dfn.lit("world"),
+ ... ).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ ['hello', None]
+ """
+ null_expr = null_string.expr if null_string is not None else None
+ return Expr(f.string_to_array(string.expr, delimiter.expr, null_expr))
+
+
+def string_to_list(
+ string: Expr, delimiter: Expr, null_string: Expr | None = None
+) -> Expr:
+ """Splits a string based on a delimiter and returns an array of parts.
+
+ See Also:
+ This is an alias for :py:func:`string_to_array`.
+ """
+ return string_to_array(string, delimiter, null_string)
+
+
+def gen_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr:
+ """Creates a list of values in the range between start and stop.
+
+ Unlike :py:func:`range`, this includes the upper bound.
+
+ Examples:
+ >>> ctx = dfn.SessionContext()
+ >>> df = ctx.from_pydict({"a": [0]})
+ >>> result = df.select(
+ ... dfn.functions.gen_series(
+ ... dfn.lit(1), dfn.lit(5),
+ ... ).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ [1, 2, 3, 4, 5]
+
+ Specify a custom ``step``:
+
+ >>> result = df.select(
+ ... dfn.functions.gen_series(
+ ... dfn.lit(1), dfn.lit(10), step=dfn.lit(3),
+ ... ).alias("result"))
+ >>> result.collect_column("result")[0].as_py()
+ [1, 4, 7, 10]
+ """
+ step_expr = step.expr if step is not None else None
+ return Expr(f.gen_series(start.expr, stop.expr, step_expr))
+
+
+def generate_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr:
+ """Creates a list of values in the range between start and stop.
+
+ Unlike :py:func:`range`, this includes the upper bound.
+
+ See Also:
+ This is an alias for :py:func:`gen_series`.
+ """
+ return gen_series(start, stop, step)
+
+
def flatten(array: Expr) -> Expr:
"""Flattens an array of arrays into a single array.
diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py
index f25c6e7..2100da9 100644
--- a/python/tests/test_functions.py
+++ b/python/tests/test_functions.py
@@ -331,6 +331,10 @@
lambda data: [len(r) == 0 for r in data],
),
(
+ f.list_empty,
+ lambda data: [len(r) == 0 for r in data],
+ ),
+ (
lambda col: f.array_extract(col, literal(1)),
lambda data: [r[0] for r in data],
),
@@ -355,18 +359,54 @@
lambda data: [1.0 in r for r in data],
),
(
+ lambda col: f.list_has(col, literal(1.0)),
+ lambda data: [1.0 in r for r in data],
+ ),
+ (
+ lambda col: f.array_contains(col, literal(1.0)),
+ lambda data: [1.0 in r for r in data],
+ ),
+ (
+ lambda col: f.list_contains(col, literal(1.0)),
+ lambda data: [1.0 in r for r in data],
+ ),
+ (
lambda col: f.array_has_all(
col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
),
lambda data: [np.all([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
),
(
+ lambda col: f.list_has_all(
+ col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
+ ),
+ lambda data: [np.all([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
+ ),
+ (
lambda col: f.array_has_any(
col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
),
lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
),
(
+ lambda col: f.list_has_any(
+ col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
+ ),
+ lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
+ ),
+ (
+ lambda col: f.arrays_overlap(
+ col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
+ ),
+ lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
+ ),
+ (
+ lambda col: f.list_overlap(
+ col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]])
+ ),
+ lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data],
+ ),
+ (
lambda col: f.array_position(col, literal(1.0)),
lambda data: [py_indexof(r, 1.0) for r in data],
),
@@ -419,10 +459,18 @@
lambda data: [arr[:-1] for arr in data],
),
(
+ f.list_pop_back,
+ lambda data: [arr[:-1] for arr in data],
+ ),
+ (
f.array_pop_front,
lambda data: [arr[1:] for arr in data],
),
(
+ f.list_pop_front,
+ lambda data: [arr[1:] for arr in data],
+ ),
+ (
lambda col: f.array_remove(col, literal(3.0)),
lambda data: [py_arr_remove(arr, 3.0, 1) for arr in data],
),
@@ -1760,3 +1808,92 @@
def test_conditional_functions(df_with_nulls, expr, expected):
result = df_with_nulls.select(expr.alias("result")).collect()[0]
assert result.column(0) == expected
+
+
+@pytest.mark.parametrize("func", [f.array_any_value, f.list_any_value])
+def test_any_value_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [[None, 2, 3], [None, None, None], [1, 2, 3]]})
+ result = df.select(func(column("a")).alias("v")).collect()
+ values = [row.as_py() for row in result[0].column(0)]
+ assert values[0] == 2
+ assert values[1] is None
+ assert values[2] == 1
+
+
+@pytest.mark.parametrize("func", [f.array_distance, f.list_distance])
+def test_array_distance_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [[1.0, 2.0]], "b": [[1.0, 4.0]]})
+ result = df.select(func(column("a"), column("b")).alias("v")).collect()
+ assert result[0].column(0)[0].as_py() == pytest.approx(2.0)
+
+
+@pytest.mark.parametrize(
+ ("func", "expected"),
+ [
+ (f.array_max, [5, 10]),
+ (f.list_max, [5, 10]),
+ (f.array_min, [1, 2]),
+ (f.list_min, [1, 2]),
+ ],
+)
+def test_array_min_max(func, expected):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [[1, 5, 3], [10, 2]]})
+ result = df.select(func(column("a")).alias("v")).collect()
+ values = [row.as_py() for row in result[0].column(0)]
+ assert values == expected
+
+
+@pytest.mark.parametrize("func", [f.array_reverse, f.list_reverse])
+def test_array_reverse_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [[1, 2, 3], [4, 5]]})
+ result = df.select(func(column("a")).alias("v")).collect()
+ values = [row.as_py() for row in result[0].column(0)]
+ assert values == [[3, 2, 1], [5, 4]]
+
+
+@pytest.mark.parametrize("func", [f.arrays_zip, f.list_zip])
+def test_arrays_zip_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]})
+ result = df.select(func(column("a"), column("b")).alias("v")).collect()
+ values = result[0].column(0)[0].as_py()
+ assert values == [{"c0": 1, "c1": 3}, {"c0": 2, "c1": 4}]
+
+
+@pytest.mark.parametrize("func", [f.string_to_array, f.string_to_list])
+def test_string_to_array_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": ["hello,world,foo"]})
+ result = df.select(func(column("a"), literal(",")).alias("v")).collect()
+ assert result[0].column(0)[0].as_py() == ["hello", "world", "foo"]
+
+
+def test_string_to_array_with_null_string():
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": ["hello,NA,world"]})
+ result = df.select(
+ f.string_to_array(column("a"), literal(","), literal("NA")).alias("v")
+ ).collect()
+ values = result[0].column(0)[0].as_py()
+ assert values == ["hello", None, "world"]
+
+
+@pytest.mark.parametrize("func", [f.gen_series, f.generate_series])
+def test_gen_series_aliases(func):
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [0]})
+ result = df.select(func(literal(1), literal(5)).alias("v")).collect()
+ assert result[0].column(0)[0].as_py() == [1, 2, 3, 4, 5]
+
+
+def test_gen_series_with_step():
+ ctx = SessionContext()
+ df = ctx.from_pydict({"a": [0]})
+ result = df.select(
+ f.gen_series(literal(1), literal(10), literal(3)).alias("v")
+ ).collect()
+ assert result[0].column(0)[0].as_py() == [1, 4, 7, 10]