Skip to content

Centralize higher-order list lambda evaluation helpers #23667

Description

@kosiew

Related PR

#23267

Problem

array_filter, array_any_match, and array_first each implement their own version of the same higher-order list execution pattern:

  1. normalize a list-like argument to List / LargeList,
  2. extract the flattened child values with slice-aware semantics,
  3. evaluate a lambda once over the flattened values,
  4. spread captured outer columns to flattened row cardinality with list_values_row_number,
  5. map the flattened lambda result back to one output value per input row using adjusted offsets and null-row handling.

Some common pieces already live in datafusion/functions-nested/src/lambda_utils.rs, but the predicate-evaluation and row-remapping contracts are still duplicated in individual functions such as:

  • datafusion/functions-nested/src/array_filter.rs
  • datafusion/functions-nested/src/array_any_match.rs
  • datafusion/functions-nested/src/array_first.rs

This makes subtle behavior easy to drift across functions: sliced lists, null list rows, empty lists, nullable elements, List vs LargeList, and coerced FixedSizeList / list-view inputs all need consistent treatment.

Why it matters

Higher-order list functions are boundary code between logical lambda expressions and Arrow array layout. Small mistakes here usually surface as correctness bugs only for edge cases: sliced arrays, null rows with non-empty backing child values, empty rows, or captured outer columns.

Duplicated flatten/evaluate/spread logic also raises the cost of adding new higher-order array functions. Each new function must rediscover the same offset and null invariants, and reviewers must re-check the same contracts again.

A shared helper would improve:

  • correctness consistency across higher-order list functions,
  • reviewability for new functions,
  • coverage of list-like input coercions,
  • future support for additional Arrow list layouts.

Invariant / desired behavior

All single-list higher-order functions that evaluate a lambda over list elements should share one canonical execution contract:

  • lambda input cardinality equals the number of reachable flattened elements for the input rows,
  • captured outer arrays are expanded to the same flattened cardinality using the list row mapping,
  • sliced list arrays use offsets relative to the sliced child values,
  • null list rows do not expose unreachable child values to the lambda after clear_null_values handling,
  • empty and null rows map back deterministically according to each function's result semantics,
  • List and LargeList behavior stays equivalent except for offset width,
  • coercions from FixedSizeList, ListView, and LargeListView remain explicit and tested where advertised.

Proposed direction

Introduce a small shared helper in datafusion/functions-nested/src/lambda_utils.rs for the common single-list lambda evaluation path.

The helper should not decide each function's final result semantics. Instead, it should centralize only the mechanical boundary work:

  • validate/extract the (list, lambda) pair,
  • materialize the list argument for number_rows,
  • handle fully-null fast paths where appropriate or expose enough state for callers to do so,
  • extract slice-aware flattened list values,
  • evaluate the lambda over those values,
  • spread captured arrays with list_values_row_number,
  • validate the expected lambda result type when the caller asks for a boolean predicate,
  • expose adjusted per-row ranges over the flattened result.

Possible shape:

  • a SingleListLambdaInput / EvaluatedListLambda struct containing:
    • original list array,
    • flattened values array,
    • evaluated lambda result array,
    • row ranges based on adjusted offsets,
    • original null bitmap / row validity metadata.
  • a boolean-predicate convenience wrapper for functions like array_filter, array_any_match, and array_first.

Then migrate one function at a time, preserving behavior.

Scope

In

  • Add shared helper(s) for single-list higher-order lambda evaluation in lambda_utils.rs or a nearby module.
  • Migrate array_first, array_any_match, and array_filter to use the helper where it reduces duplicated flatten/evaluate/spread code.
  • Keep each function's result-specific logic local:
    • array_filter: build filtered list output,
    • array_any_match: reduce predicate ranges to boolean/null,
    • array_first: choose first matching element index and take it.
  • Preserve existing public SQL behavior and aliases.
  • Add focused regression tests for shared edge cases:
    • sliced List,
    • null list rows with non-empty child values,
    • empty list rows,
    • nullable elements and nullable predicates,
    • captured outer columns,
    • List and LargeList parity.
  • Add coverage for any list-like coercions the helper claims to support.

Out

  • Do not change lambda evaluation from eager/vectorized to row-local short-circuiting.
  • Do not add new SQL functions.
  • Do not redesign the higher-order UDF trait API unless the helper cannot be implemented locally.
  • Do not change documented null semantics for existing functions.
  • Do not attempt broad support for unsupported Arrow layouts unless covered by explicit tests and contracts.

Acceptance criteria

  • array_filter, array_any_match, and array_first no longer duplicate the core flatten/evaluate/spread/captured-column logic.
  • A single helper owns the contract for flattened lambda input cardinality and captured-column expansion.
  • Existing tests for the migrated functions pass without behavior changes.
  • New regression tests cover sliced lists, null rows, empty rows, nullable elements, captured outer columns, and List / LargeList parity through the shared path.
  • Any advertised FixedSizeList, ListView, or LargeListView coercion behavior is either tested or explicitly not claimed by the helper.
  • Error messages for non-boolean predicates in predicate-based functions remain clear and function-specific.

Tests / verification

  • Unit tests in datafusion-functions-nested for the helper and migrated functions.
  • SQLLogicTests for representative SQL behavior if migration changes user-visible paths.
  • Existing targeted checks:
    • cargo test -p datafusion-functions-nested array_filter
    • cargo test -p datafusion-functions-nested array_any_match
    • cargo test -p datafusion-functions-nested array_first
    • relevant datafusion-sqllogictest array test files.

Notes / open questions

  • lambda_utils.rs already contains shared pieces (coerce_single_list_arg, single_list_lambda_parameters, extract_list_values). The issue is not to replace those, but to extend sharing to the higher-risk lambda evaluation and row-range mapping path.
  • The helper should make eager predicate evaluation explicit. Short-circuit evaluation, if desired later, should be a separate design issue because it changes execution semantics and optimization assumptions.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions