Skip to content

test(spark): tail-sweep coverage for low-coverage datasource classes - #19405

Merged
voonhous merged 3 commits into
apache:masterfrom
yihua:test-spark-datasource-tail-sweep-2
Jul 31, 2026
Merged

test(spark): tail-sweep coverage for low-coverage datasource classes#19405
voonhous merged 3 commits into
apache:masterfrom
yihua:test-spark-datasource-tail-sweep-2

Conversation

@yihua

@yihua yihua commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Part 2 of the Spark-datasource small-class coverage tail-sweep (sibling to #19164). Several small classes in the Spark datasource had low unit-test coverage, with branches reached only indirectly, if at all. This adds focused, behavior-pinning unit tests for the genuinely uncovered ones.

Summary and Changelog

Adds unit coverage for three low-coverage classes. Every assertion pins exact output, so a wrong result would fail the test.

  • org.apache.spark.sql.BaseHoodieCatalystExpressionUtils (0 missed lines, but many uncovered branches): new TestHoodieCatalystExpressionUtils drives tryMatchAttributeOrderingPreservingTransformation across the whole OrderPreservingTransformation match. It asserts the exact source AttributeReference recovered for identity, arithmetic on either operand, unary math, string case, date add/sub, date parsing (the per-Spark-version ParseToDate hook), and order-preserving up-cast, and that non-order-preserving shapes (numeric-to-string cast, attribute-free arithmetic, a non-whitelisted Sqrt) do not match. The narrowing-cast case is pinned to current behavior with a TODO referencing Data skipping treats order-breaking casts as order-preserving (isCastPreservingOrdering) #19445.
  • org.apache.spark.sql.avro.AvroUtils: new TestAvroUtils covers supportsDataType (atomic, struct, array, map, null supported; CalendarInterval and its wrappers unsupported) and the AvroSchemaHelper matching and validation paths previously exercised only through the Avro serializers: non-RECORD rejection, by-name vs positional field lookup, extra-Catalyst-field and extra-required-Avro-field validation (including the ignoreNullable and nullable-Avro-field skips), and the ambiguous case-insensitive by-name match. Each error case pins the raised IncompatibleSchemaException message.
  • org.apache.spark.sql.FileFormatUtilsForFileGroupReader: new TestFileFormatUtilsForFileGroupReader covers applyFiltersToPlan, pinning the Catalyst expression produced for each pushed-down data-source Filter (comparisons, null checks, In, string predicates, AlwaysTrue / AlwaysFalse, a nested and/or/not tree, and multi-filter And), the NoSuchElementException raised for a filter on a column absent from the table schema, and that an empty filter list returns the input plan unchanged. applyNewFileFormatChanges in the same object (the fgReader plan-rewrite entry point) is intentionally out of scope here.

Candidates verified and left out: VectorDistanceUtils is already exhaustively covered with exact-value assertions by TestHoodieVectorSearchFunction, so no new test was added. HiveSyncProcedure and HoodieNestedSchemaPruning need heavier end-to-end scaffolding (hive metastore, optimizer plan fixtures) and are deferred to a separate pass. No code was copied.

Impact

Test-only. No production code changes, no public API change, and no behavior change.

Risk Level

none

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

@github-actions github-actions Bot added the size:L PR with lines of changes in (300, 1000] label Jul 29, 2026
@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.81%. Comparing base (633d142) to head (a541fb7).
⚠️ Report is 17 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19405      +/-   ##
============================================
+ Coverage     75.56%   75.81%   +0.24%     
- Complexity    32651    32865     +214     
============================================
  Files          2574     2576       +2     
  Lines        142995   143558     +563     
  Branches      17530    17776     +246     
============================================
+ Hits         108051   108835     +784     
+ Misses        26908    26696     -212     
+ Partials       8036     8027       -9     
Components Coverage Δ
hudi-common 82.26% <ø> (-0.02%) ⬇️
hudi-client 81.82% <ø> (-0.01%) ⬇️
hudi-flink 83.97% <ø> (-0.06%) ⬇️
hudi-spark-datasource 68.66% <ø> (+0.32%) ⬆️
hudi-utilities 73.66% <ø> (+2.46%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.49% <ø> (ø)
hudi-sync 70.87% <ø> (+0.14%) ⬆️
hudi-io 79.60% <ø> (+0.02%) ⬆️
hudi-timeline-service 83.74% <ø> (+0.29%) ⬆️
hudi-cloud 64.00% <ø> (ø)
hudi-kafka-connect 53.96% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 49.50% <ø> (+0.27%) ⬆️
flink-integration-tests 48.80% <ø> (-0.06%) ⬇️
hadoop-mr-java-client 43.42% <ø> (+0.09%) ⬆️
integration-tests 13.57% <ø> (-0.06%) ⬇️
spark-client-hadoop-common 48.68% <ø> (-0.04%) ⬇️
spark-java-tests 51.26% <ø> (-0.13%) ⬇️
spark-scala-tests 45.96% <ø> (-0.13%) ⬇️
utilities 36.56% <ø> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 85 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yihua
yihua marked this pull request as ready for review July 30, 2026 19:30

@hudi-agent hudi-agent 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.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR adds behavior-pinning unit tests for three low-coverage Spark-datasource classes: the order-preserving transformation matcher in BaseHoodieCatalystExpressionUtils, AvroUtils/AvroSchemaHelper, and FileFormatUtilsForFileGroupReader.applyFiltersToPlan. I traced each assertion against the actual source implementations (including isCastPreservingOrdering, the OrderPreservingTransformation extractor cases, the translate/reduceLeft(And) filter lowering, and the Avro schema-matching/validation error messages) and every expected value lines up with real behavior. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. One minor naming nit in TestAvroUtils; the other two files are clean.

cc @yihua

yihua added 2 commits July 30, 2026 23:29
Add unit coverage for BaseHoodieCatalystExpressionUtils order-preserving
transformation matching, org.apache.spark.sql.avro.AvroUtils schema
matching and validation, and FileFormatUtilsForFileGroupReader filter
lowering. Each assertion pins the exact translated output.
Mirror avroWithOptionalGhost so the optional-ghost scenario reads clearly at a glance.
@yihua
yihua force-pushed the test-spark-datasource-tail-sweep-2 branch from 4eb7fe2 to 6688e7b Compare July 31, 2026 06:41

@voonhous voonhous 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.

Did a Claude-assisted pass beyond the earlier bot review; everything below was verified against the checked-out branch before commenting.

Verified clean (no action needed):

  • Diff is test-only: 3 new files, 361 insertions, 0 deletions; no accidental reverts.
  • All 23 tests pass locally (mvn -o test -pl hudi-spark-datasource/hudi-spark -Punit-tests -Dspark3.5 -Dtest='TestAvroUtils,TestFileFormatUtilsForFileGroupReader,TestCatalystExpressionOrderPreserving'), with no SparkSession required.
  • Cross-profile compile risk cleared: every Spark expression the tests construct was probe-compiled against 3.3.4/3.4.3/3.5.5/4.0.2/4.1.1 with zero errors; AvroUtils and the order-preserving matcher are single-sourced in hudi-spark-common (#19147/#19149), and no spark-avro jar is on the test classpath to shadow the vendored copy.
  • No duplicate coverage: none of the three classes had direct tests, and there is no overlap with #19164. Nearest neighbor TestConvertFilterToCatalystExpression targets a different translator.

Inline comments, ranked: 2 correctness-tier coverage gaps (the order-breaking casts that isCastPreservingOrdering wrongly accepts, and the per-version ParseToDate/ParseToTimestamp hook -- the only version-divergent branch of the matcher), 3 assertion-strength fixes with suggestions, and a few optional nits.

// Widening a numeric column preserves ordering, so the source attribute is recovered.
assertEquals(Some(intAttr), matched(Cast(intAttr, LongType)))
// Casting a numeric column to string can reorder values, so it must not match.
assertEquals(None, matched(Cast(intAttr, StringType)))

@voonhous voonhous Jul 31, 2026

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.

Both Cast cases pin the safe directions, but the discriminating one is missing: HoodieSparkTypeUtils.isCastPreservingOrdering only rejects String<->Numeric and returns true for everything else, including narrowing numeric casts. Adding

val longAttr = AttributeReference("l", LongType)()
assertEquals(None, matched(Cast(longAttr, IntegerType)))

fails today: the matcher recovers the attribute even though non-ANSI narrowing wraps around, and DataSkippingUtils.translateIntoColumnStatsIndexFilterExpr then rewrites min/max through the cast. Concrete failure: bigint col a in a file with values {1, 2147483647, 4294967297} (min=1, max=4294967297); the filter cast(a as int) > 100 is translated to cast(a_maxValue as int) > 100, and cast(4294967297L as int) wraps to 1, so the file is pruned even though it holds a=2147483647 whose cast is 2147483647 -> silently missing rows. Same family: the Multiply/Divide arms match any literal operand, including negative ones that reverse ordering.

Since the missing assertion exposes a production bug, please file a GitHub issue on isCastPreservingOrdering (numeric-to-numeric should require Cast.canUpCast), and either fix it in this PR or pin current behavior here with a TODO referencing the issue so the hazard is documented rather than invisible.

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.

Pinned Cast(long -> int) as-is with a TODO pointing at #19445. Fixing isCastPreservingOrdering would pull DataSkippingUtils changes into a test-only PR, so the fix rides with the issue.

}

@Test
def testDateTransformationsPreserveOrdering(): Unit = {

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.

The file covers only shared match arms, not the one branch of this matcher that differs per Spark version: unapplyOrderPreservingDateParsing (BaseHoodieCatalystExpressionUtils.scala:107-110), whose overrides pattern-match ParseToDate/ParseToTimestamp with different arities in each version module (3.3: ParseToDate(child,_,_); 3.5/4.x: ParseToDate(child,_,_,_)), re-implemented in #19149. That is exactly the branch a per-profile unit test should pin.

Related data point worth a follow-up: both nodes are RuntimeReplaceable, and Spark's first optimizer batch (ReplaceExpressions) rewrites them before filters reach data skipping, so this branch may never fire on real queries; the existing to_timestamp coverage in TestDataSkippingUtils (line 677) only survives because that harness applies OptimizeIn alone.

Action: add one case built portably so it compiles on every profile, e.g.

assertEquals(Some(strAttr),
  matched(sparkAdapter.getExpressionFromColumn(
    functions.to_date(sparkAdapter.createColumnFromExpression(strAttr)))))

(SparkAdapter.scala:376,384 provide both directions), and consider a follow-up issue to confirm whether to_date/to_timestamp data skipping still works post-ReplaceExpressions.

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.

Added a ParseToDate case. Went with the 1-arg auxiliary constructor instead of the adapter round-trip; javap shows it stable on 3.3.1 through 4.1.1 and it keeps functions out of the test. Reachability question filed as #19446.

- rename TestCatalystExpressionOrderPreserving to TestHoodieCatalystExpressionUtils
- pin narrowing-cast behavior with a TODO referencing apache#19445
- cover the per-version ParseToDate hook via its stable 1-arg constructor
- add GreaterThanOrEqual/LessThan/sources.And arms and an unknown-column failure pin
- use three filters so the And fold direction is observable
- reorder Avro fields so by-name lookup is discriminated from positional
- cover ignoreNullable=true with a non-nullable extra Catalyst field
- pin SQLConf case sensitivity and cover the case-sensitive resolver half
- wrap must-not-throw calls in assertDoesNotThrow
- mark dead vendored paths (supportsDataType, positional matching) as drift guards

@voonhous voonhous 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.

LGTM

@voonhous
voonhous enabled auto-merge (squash) July 31, 2026 11:58
@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@voonhous
voonhous merged commit 1ba9a58 into apache:master Jul 31, 2026
74 of 75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants