Skip to content

[SPARK-59068][4.3][SQL] Correct runtime filter attribute handling - #58412

Open
szehon-ho wants to merge 4 commits into
apache:branch-4.3from
szehon-ho:codex/runtime-filter-nested-compat-4.3
Open

[SPARK-59068][4.3][SQL] Correct runtime filter attribute handling#58412
szehon-ho wants to merge 4 commits into
apache:branch-4.3from
szehon-ho:codex/runtime-filter-nested-compat-4.3

Conversation

@szehon-ho

@szehon-ho szehon-ho commented Aug 29, 2026

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

This PR backports the corrected runtime-filter attribute handling from #58370 to branch-4.3.

It:

  • allows nested references from ordinary filterAttributes() and preserves nested access in the pushed filter;
  • keeps fullyPushedFilterAttributes() limited to top-level attributes;
  • resolves and caches fully-pushed attributes during normal scan planning;
  • reports invalid runtime-filter attributes with structured errors;
  • enables nested runtime group filtering for row-level operations;
  • documents how nested attribute paths are represented in V1 filters; and
  • adds Catalyst, V1, V2, DPP, invalid-attribute, and row-level operation coverage.

This is a net backport. It adapts the final behavior to the 4.3 codebase without pulling in unrelated fixture refactoring from later branches.

Why are the changes needed?

branch-4.3 contains the unreleased SupportsRuntimeCatalystFiltering API, so this backport aligns its runtime-filter attribute behavior with the corrected implementation.

Does this PR introduce any user-facing change?

Yes. Runtime-filtering scans may report nested references from ordinary filterAttributes(). Nested references from fullyPushedFilterAttributes() remain unsupported, and invalid fully-pushed attributes are rejected consistently during planning.

For SupportsRuntimeCatalystFiltering, this corrects behavior on the unreleased Spark 4.3 branch. Method signatures and binary compatibility are unchanged.

How was this patch tested?

  • SPARK_GENERATE_GOLDEN_FILES=1 build/sbt 'core/testOnly *SparkThrowableSuite -- -t "Error conditions are correctly formatted"'
  • build/sbt 'core/testOnly org.apache.spark.SparkThrowableSuite'
  • build/sbt 'sql/testOnly org.apache.spark.sql.connector.DataSourceV2CatalystRuntimeFilterSuite'
  • Focused nested DPP tests in DataSourceV2SQLSuiteV1Filter and DataSourceV2SQLSuiteV2Filter
  • GroupBasedRowLevelOperationCatalystRuntimeFilterSuite
  • DeltaBasedRowLevelOperationCatalystRuntimeFilterSuite
  • catalyst/scalastyle
  • sql/scalastyle
  • catalyst/checkstyle
  • build/sbt 'sql/testOnly org.apache.spark.sql.connector.DataSourceV2CatalystRuntimeFilterSuite -- -z "missing fully pushed filter attribute"'

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Codex with GPT-5

@szehon-ho

Copy link
Copy Markdown
Member Author

@cloud-fan @dongjoon-hyun @sunchao @HeartSaVioR can you help review (this is minimal DSV2 API-facing backport for Spark 4.3 to prevent breaking change)

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed 7d6278d with five independent agents. One source-established issue in the in-memory test connector is noted inline; no additional production-code findings. Spark test suites were not run locally.

Comment on lines +780 to +783
readSchema.findNestedField(path, resolver = resolver)
.orElse(tableSchema.findNestedField(path, resolver = resolver)).map {
case (_, f) =>
path -> AttributeReference(ref.fieldNames.mkString("."), f.dataType, f.nullable)()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Preserve transform semantics for nested partition filters

partitioning.flatMap(_.references()) includes non-identity transforms, but this nested lookup binds each source field as if its original value occupied the partition-key slot. For the fixture's supported PARTITIONED BY (truncate(s.part, 1)), a row with s.part = 'AB' is stored under key 'A' by getKey. A scalar-subquery runtime predicate such as s.part = (SELECT max(val) FROM dim), where max(val) is 'AB', is then remapped to 'A' = 'AB', and filter() drops the matching partition before residual evaluation can recover it. Before this change the fixture did not advertise s.part, so the row survived for the residual filter.

Please restrict direct key-slot binding to identity transforms while preserving actual partition-key ordinals, or project predicates through the transform. This finding is limited to the in-memory test connector and was independently checked by two reviewers from source; it was not reproduced at runtime.

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.

Thanks, good catch. The fixture now binds only identity transforms and uses each transform’s actual partition-key ordinal. Predicates on transformed source columns remain for residual evaluation. I added a regression covering a nested truncate transform in slot 0 and a nested identity transform in slot 1.

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rereviewed 51ee79a with five independent agents. The original Catalyst example is fixed and its regression test passed in CI. Two remaining findings are noted inline, both limited to the in-memory test connectors and established from source. No local Spark test suites or runtime reproductions were run.

Comment on lines 69 to +72
override def filterAttributes(): Array[NamedReference] = {
val scanFields = readSchema.fields.map(_.name).toSet
partitioning.flatMap(_.references)
.filter(ref => scanFields.contains(ref.fieldNames.mkString(".")))
.filter(ref => readSchema.findNestedField(
ref.fieldNames.toImmutableArraySeq, resolver = SQLConf.get.resolver).isDefined)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Apply transform safety to the public V1/V2 fixtures

Both public fixtures still advertise nested source references from every partition transform, but their evaluators compare source predicate values directly against transformed partition keys. With PARTITIONED BY (truncate(s.part, 1)), a row containing s.part = 'AB' is stored under key 'A'. A V2 predicate such as s.part = (SELECT max(value) FROM dim), with dim.value = 'AB', removes that matching partition. V1/V2 DPP has the same problem when it pushes IN ('AB'). The equivalent V1 declaration is in InMemoryBaseTable.filterAttributes().

The base did not advertise nested s.part, so this pruning path was not enabled. The new identity-transform guard applies only to CatalystRuntimeFilteringScan; it does not protect these public-filter evaluators. Please restrict them to supported identity transforms or translate source predicates through the partition transform. Retained residual filters cannot recover a partition already removed.

This finding is limited to the in-memory test connectors and is established from source, without runtime reproduction.

@szehon-ho szehon-ho Aug 31, 2026

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.

Thanks, good catch. I restricted both public V1/V2 fixtures to advertise only identity-transform source attributes, and added the same identity guard in their evaluators. The shared regression uses truncate(derives.toStr, 1) and runs against both fixtures; it verifies no DPP filter is pushed on the transformed source and that all partitions remain available for the residual join.

Comment on lines +98 to +99
override def fullyPushedFilterAttributes(): Array[NamedReference] = {
val fullyPushedFilterAttrs = Option(
InMemoryCatalystRuntimeFilterTable.this.properties.get(FullyPushedFilterAttributesKey))
.map(_.split(",").map(_.trim).toSet)
.getOrElse(Set.empty)
filterAttributes().filter { ref =>
fullyPushedFilterAttrs.contains(ref.fieldNames.mkString("."))
}
partitionAttrs.filter(ref => fullyPushedFilterAttrs.contains(ref.fieldNames.mkString(".")))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Align fully pushed declarations with the new evaluator

fullyPushedFilterAttributes() still advertises source columns of non-identity transforms, but the new evaluator skips those transforms. For a DATE part column partitioned by days(part) with TBLPROPERTIES('fully-pushed-filter-attributes' = 'part'), a predicate such as part = (SELECT max(value) FROM dim) selecting one of two dates has its residual filter removed by DataSourceV2Strategy. The fixture then returns without filtering because no identity slots exist, so both dates are returned.

This worked on the base: getKey preserves the original DATE day value for DaysTransform, so the old evaluator correctly evaluated the source predicate. Please restrict fully pushed declarations to sources the evaluator actually enforces, or retain their correct evaluation.

This is a regression in the test connector introduced by the follow-up commit and is established from source, without runtime reproduction.

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.

Thanks. fullyPushedFilterAttributes() is now restricted to identity transforms, matching what the fixture evaluator can enforce directly. Non-identity sources may still receive ordinary Catalyst runtime filters, but Spark retains the post-scan residual filter. I added a days(part) regression verifying the runtime filter is pushed, both partitions remain at the source, and the residual returns only the matching date.

@szehon-ho szehon-ho changed the title [SPARK-59068][4.3][SQL] Restore support for nested runtime filter attributes [SPARK-59068][4.3][SQL] Correct runtime filter attribute handling Aug 31, 2026
@szehon-ho

Copy link
Copy Markdown
Member Author

Added the two follow-ups from cloud-fan's post-merge review of #58370:

  • Corrected the V1 runtime-filter Javadoc to describe dot-separated paths with each path part quoted as needed (review comment).
  • Resolved and cached fullyPushedFilterAttributes() from the primary runtimeFilterAttrs planning path, and updated the regression to exercise that entry point (review comment).

The focused regression passes.

@szehon-ho

szehon-ho commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

hi @sunchao , can you take another look when you get a chance (for 4.3) ? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants