Skip to content

fix(spark): widen procedure filter numeric comparisons - #19836

Merged
voonhous merged 15 commits into
apache:masterfrom
w3lld1:fix-procedure-filter-numeric-coercion
Sep 7, 2026
Merged

fix(spark): widen procedure filter numeric comparisons#19836
voonhous merged 15 commits into
apache:masterfrom
w3lld1:fix-procedure-filter-numeric-coercion

Conversation

@w3lld1

@w3lld1 w3lld1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Procedure filters narrowed Long columns to Int and left other mixed numeric expressions unresolved, so large Long values matched wrongly and reversed or mixed-type comparisons failed validation.

Fixes #19632. Related decimal parity work: #19860.

Summary and Changelog

  • Widen numeric comparison operands to their common type with TypeCoercion or AnsiTypeCoercion, chosen by SQLConf.get.ansiEnabled, instead of narrowing the column.
  • Apply Spark's DecimalPrecision rules before the generic widening, for comparisons and for arithmetic with decimal operands, so literal precision and decimal result scale match Spark.
  • Widen IN, <=>, coalesce and arithmetic operands; non-decimal / promotes to Double, div promotes narrow integrals to BIGINT, and a null operand takes its peer's type.
  • Propagate ANSI arithmetic and cast errors instead of silently dropping the row.
  • Reject a non-boolean filter at validation, as Spark does.
  • Bind and resolve once per batch; leave unresolved operands alone so validation keeps its messages.
Before and after, by user-visible case (compares master with a6cc9da55868)

Examples use procedure output columns: ts is BIGINT, price is DOUBLE, and dec is DECIMAL. "Rejected" means the procedure fails filter validation before returning results. Matching behavior has been checked on Spark 3.5.5 and 4.1.1 for the covered cases and settings.

User-visible case Before this PR After this PR Spark parity
Large BIGINT: ts > 2000, with ts = 3000000000 Incorrectly excludes the row: narrowing to INT wraps without ANSI or throws an exception that is swallowed with ANSI Keeps the row in both ANSI modes; the BIGINT value is preserved Matches tested cases on 3.5.5 and 4.1.1
Large BIGINT: ts < 2000, with ts = 3000000000 Incorrectly keeps the row without ANSI because narrowing wraps to a negative value Correctly excludes the row in both ANSI modes Matches tested cases on 3.5.5 and 4.1.1
Reversed or mixed-type comparisons: 1500 < ts, price > 15.0, dec > 1.00 with dec DECIMAL(10,2) Rejected because operand types are not resolved to compatible types Accepted; operands are converted using the active Spark rules Matches tested cases on 3.5.5 and 4.1.1
Numeric membership and null-safe equality: ts IN (1000, null), ts <=> 1000, ts <=> null Rejected for these mixed-type operands Accepted, with normal SQL membership and null semantics Matches tested cases on 3.5.5 and 4.1.1
Mixed arithmetic and fallback values: ts + 1 > 1500, ts / 2 > 500, coalesce(ts, 0) > 1500 Rejected for these mixed-type operands Accepted; addition preserves BIGINT capacity and non-decimal / uses Double Matches tested cases on 3.5.5 and 4.1.1
Mixed decimal arithmetic: dec + 1 > 0, dec / null > 0 Rejected Accepted using Spark's decimal promotion rules; division by null produces no matching row Matches tested cases on 3.5.5 and 4.1.1
High-precision mixed comparisons: BIGINT 3000000000 greater than a decimal literal of 10^-30; DECIMAL(38,30) value 10^-29 > 0 Rejected because the mixed operand types remain unresolved Uses Spark's specialized decimal comparison rules; both examples keep the row under the tested default precision settings and either ANSI mode Matches tested cases on 3.5.5 and 4.1.1; precision settings affect results
ANSI arithmetic failure in an otherwise valid filter: ts + 1L > 0L, with ts = Long.MaxValue Swallows the overflow and silently excludes the row Fails the procedure with the arithmetic exception, matching Spark; without ANSI, the addition still wraps and the row is excluded Matches tested ANSI and non-ANSI arithmetic error behavior
ANSI cast of a malformed string: int(name) > 1 Accepted; with ANSI the cast error is swallowed and the row excluded Fails the procedure with the cast exception under ANSI; without ANSI the cast yields null and the row is excluded Matches tested cases on 3.5.5 and 4.1.1
Non-boolean filter: ts + 1, name ts + 1 rejected as unresolved; name accepted and returns no rows Both rejected at validation as non-boolean Matches Spark, which raises FILTER_NOT_BOOLEAN for both
Integral division on an INT column: id div 2 > 0 Rejected: div accepts only BIGINT or DECIMAL and same-typed INT operands were never promoted Accepted; INT operands are promoted to BIGINT the way Spark's IntegralDivision rule does Matches tested cases on 3.5.5 and 4.1.1
Null with a non-numeric peer: name IN ('a1', null), name <=> null Rejected Accepted; the null takes the peer's type Matches tested cases on 3.5.5 and 4.1.1

Release consideration: applications that previously received an empty or incomplete result may now receive matching rows, or an arithmetic or cast error when ANSI mode is enabled. A filter whose result is not boolean, such as a bare non-boolean column, is now rejected at validation instead of returning no rows. Null results still do not match a filter. Filtering does not change the output schema or rewrite returned column values. Decimal and floating-point conversions still follow Spark's rounding and overflow rules; full SQL parity is not claimed.

Impact

Previously rejected numeric filters are accepted and wide integral values are preserved. A non-boolean filter, and under ANSI an arithmetic or cast error, now fails the procedure instead of returning no rows. No public API change; procedure filters still support a subset of Spark SQL expressions.

Remaining limits

Numeric conversion is not always lossless. Floating-point conversion can round values. Decimal conversion can round or overflow according to the active Spark rules and settings. ANSI arithmetic and cast errors now reach the caller; non-ANSI decimal cast overflow produces null and excludes the row. Non-ANSI integral arithmetic can wrap rather than return null.

The previously reported high-precision mixed comparison mismatches are fixed: a large Long compared with a tiny decimal literal on Spark 3.5.5, and a tiny DECIMAL(38,30) compared with integer zero on Spark 4.1.1. The extreme decimal/decimal example in #19860 has also been checked against SQL; it should not be described as a confirmed remaining mismatch.

Filters still run after limit in the show_* procedures, so a filter only sees the first limit rows; tracked in #19862.

Risk Level

Moderate. Changes affect numeric filter acceptance, precision, and error propagation across Spark versions. Tests compare results with Spark SQL; complete SQL parity and the full version matrix are not claimed.

Documentation Update

Coercion behavior and known verification limits are documented here and in the related issues.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable
Validation at a6cc9da55868
  • The production source compiles against catalyst 3.3.4, 3.5.5 and 4.1.1 (standalone scalac); at df61fa197caa it also compiled against 3.4.3, 4.0.2 and 4.2.0.
  • Spark 3.5.5 / Scala 2.12 / JDK 11: TestHoodieProcedureFilterUtils 26/26 with the module's test base, TestFsViewProcedure 6/6, and the show_metadata_column_stats_overlap test in TestMetadataProcedure, all through a standalone ScalaTest runner on the module classpath.
  • Spark 4.1.1 / Scala 2.13 / JDK 17: TestHoodieProcedureFilterUtils 26/26 with a stubbed test base that carries the real withSQLConf.
  • At df61fa197caa, every assertion of the two SQL-parity tests was replayed against df.filter on all six Spark versions with no divergence, and the widened expression trees were compared with the analyzer's output on 3.3.4, 3.5.5 and 4.1.1 across about 120 filters in both ANSI modes. Targeted checks of extreme decimal/decimal comparisons, including rounding-sensitive equality and ANSI overflow, matched SQL.
  • Scalastyle and git diff --check pass.
  • The suite runs 26 tests, down from 28, after folding two duplicate decimal tests into their SQL-parity siblings. Only the spark3.5 Azure lane executes this suite in CI; the other profiles are covered by the local runs above.

@github-actions github-actions Bot added the size:S PR with lines of changes in (10, 100] label Sep 3, 2026
@codecov-commenter

codecov-commenter commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.25581% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.34%. Comparing base (9903b6d) to head (a6cc9da).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...ommand/procedures/HoodieProcedureFilterUtils.scala 73.25% 6 Missing and 17 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19836      +/-   ##
============================================
- Coverage     78.35%   78.34%   -0.01%     
+ Complexity    33953    33946       -7     
============================================
  Files          2543     2543              
  Lines        141980   142052      +72     
  Branches      17220    17247      +27     
============================================
+ Hits         111245   111288      +43     
- Misses        23039    23053      +14     
- Partials       7696     7711      +15     
Components Coverage Δ
hudi-common 83.73% <ø> (-0.02%) ⬇️
hudi-client 83.21% <ø> (+<0.01%) ⬆️
hudi-flink 85.54% <ø> (-0.05%) ⬇️
hudi-spark-datasource 73.26% <73.25%> (+<0.01%) ⬆️
hudi-utilities 74.54% <ø> (+<0.01%) ⬆️
hudi-cli 15.13% <ø> (ø)
hudi-hadoop 70.80% <ø> (ø)
hudi-sync 75.95% <ø> (ø)
hudi-io 80.02% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 65.81% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.48% <0.00%> (-0.03%) ⬇️
flink-integration-tests 48.83% <ø> (-0.04%) ⬇️
hadoop-mr-java-client 44.10% <ø> (+0.03%) ⬆️
integration-tests 13.50% <0.00%> (-0.01%) ⬇️
spark-client-hadoop-common 50.58% <ø> (-0.01%) ⬇️
spark-java-tests 52.22% <0.00%> (-0.06%) ⬇️
spark-scala-tests 47.00% <73.25%> (+0.01%) ⬆️
utilities 36.53% <0.00%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ommand/procedures/HoodieProcedureFilterUtils.scala 57.80% <73.25%> (+3.56%) ⬆️

... and 13 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.

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

The direction is right: I confirmed all three cases from #19632 are fixed (data_file_size > 2000 over a 3e9 value, reversed operands, mixed numeric pairs), on Spark 3.5.5 and 4.1.1 with ANSI on and off. TypeCoercion.findWiderTypeForTwo is present in catalyst 3.3.4 / 3.4.3 / 3.5.5 / 4.0.2 / 4.1.1, so no Spark profile loses its compile.

One item is worth a look before merge: moving the type check from node shapes to left.dataType means an unresolved operand now throws during the transform instead of at eval, which defeats Or short-circuiting and turns some non-empty results into empty ones. Details inline.

The rest are smaller: In and <=> still carry the bug the comparison operators just lost, a decimal corner where the widened type clamps at precision 38, and some coverage and comment cleanup.

@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! The PR replaces the narrow Long-column-vs-Int-literal coercion in applyTypeCoercion with Spark's TypeCoercion.findWiderTypeForTwo, so mixed numeric procedure-filter comparisons are widened symmetrically and large Long values are preserved. I traced the per-row transform/eval path and the widening logic; the substantive edge cases (accessing dataType on unresolved operands, ANSI vs non-ANSI TypeCoercion differences, DecimalType.bounded precision clamping, and the test-coverage gaps) have already been raised in the existing inline discussion, and I don't have a new, non-duplicative correctness concern to add from this automated pass. Please take a look at the existing inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

@voonhous

voonhous commented Sep 6, 2026

Copy link
Copy Markdown
Member

I'll work to land this later in the day. Will take over unless OP decides to address the comments above.

@voonhous
voonhous force-pushed the fix-procedure-filter-numeric-coercion branch from 5c62235 to 5616349 Compare September 7, 2026 08:27

@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! The change replaces the narrow Long-column/Int-literal special case in applyTypeCoercion with Spark's TypeCoercion.findWiderTypeForTwo, so mixed numeric comparisons widen symmetrically instead of narrowing the column. Most of the earlier round's points (unresolved-node dataType access, ANSI/AnsiTypeCoercion divergence, decimal precision clamping, operator coverage) still look like the main things to settle; I added one more on operator coverage. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

Addresses review feedback on the numeric coercion rewrite.

- Skip coercion when either operand is unresolved. dataType throws
  there, which defeated Or short-circuiting and, once apache#19850 started
  calling the transform from validateFilterExpression, replaced the
  "Invalid column references" and "Unsupported functions" messages
  with "Invalid call to dataType on unresolved object".
- Extend the transform to In and EqualNullSafe, which still carried
  the apache#19632 silent drop that the binary comparisons just lost.
- Pick AnsiTypeCoercion or TypeCoercion from the session's ANSI mode.
  They disagree on BIGINT with FLOAT: DOUBLE under ANSI, FLOAT under
  numericPrecedence without it.
- Leave all-decimal comparisons alone. DecimalType.bounded clamps
  precision at 38, so widening can overflow the integral side, and
  decimal ordering is already precision- and scale-independent.
- Return the original node when no operand needs a cast, and bind and
  resolve once per batch rather than once per row.

Tests run both halves of apache#19632 over one fixture, add a second row so
the numeric-pair assertions can fail, and cover In, EqualNullSafe,
reversed operands, and the decimal and ANSI paths. TestFsViewProcedure
gains a show_fsview_all filter on data_file_size, the column named in
the report.
Widening picks a wider type but not always a lossless one, and neither
case surfaces an error: an integral widened to float rounds, and a
decimal operand whose scale leaves too few integral digits overflows
the cast into a dropped row. Both are latent or benign today; record
the shapes so a future reader does not rediscover them.
@github-actions github-actions Bot added size:M PR with lines of changes in (100, 300] and removed size:S PR with lines of changes in (10, 100] labels Sep 7, 2026
Same two cases, fewer words: rounding on a float conversion and
overflow on a decimal with too little room before the point.
Fold the caveat back into the scaladoc and wrap it at the
file's ~95-column prose width. It was a short-wrapped block
of // lines sitting between the scaladoc and the method, so
it broke mid-clause and did not render as documentation.
Second review round on the procedure filter coercion.

- Drop the all-decimal skip. Leaving decimal operands uncoerced
  keeps the comparison unresolved, and validateFilterExpression
  rejects the filter before any procedure evaluates it, so
  "dec > 1.00" threw where the first commit accepted it. The
  skip also did not prevent the clamp it cited: a BIGINT against
  a DECIMAL(31,30) still widens to DECIMAL(38,30).
- Widen a NULL operand along with the numeric ones, matching the
  plan Spark builds for "ts IN (1000, null)" and "ts <=> null".
- Extend the transform to arithmetic and coalesce, which resolve
  by exact type equality and so rejected "ts + 1 > 1500".
- Correct the unresolved-operand comment. It claimed such
  expressions are rejected up front by validateFilterExpression,
  which calls this code, so nothing has been rejected yet.
- Note why the coercion rules are read from SQLConf.get: Cast
  takes its eval mode from the same thread-local, and the
  two-argument Cast is the only form portable across 3.3 to 4.x.

Tests add the validate half for every shape whose validation
result this PR flips, since procedures validate before they
filter. Also covers null operands, mixed-width IN lists,
column-against-column widening under both ANSI modes, arithmetic,
coalesce, quoted column names, and Or short-circuiting. The
TestFsViewProcedure filters now use shapes master rejects plus
one that keeps no rows, so an ignored filter cannot pass.
@voonhous

voonhous commented Sep 7, 2026

Copy link
Copy Markdown
Member

Pushed f24b3a35e84d. Most of it walks back a suggestion of mine from the last round.

I had asked for an all-decimal skip in widenNumericOperands to sidestep the DecimalType.bounded precision-38 clamp. That was wrong. Every filterable procedure calls validateFilter before applyFilter (BaseProcedure.scala:123-139, 17 of 17), and an uncoerced decimal-vs-decimal comparison is resolved == false, so validateFilterExpression rejects it and the filter throws. dec > 1.00 worked in @w3lld1's original commit and stopped working once I added the skip. The skip also did not avoid the clamp it cited -- BIGINT against DECIMAL(31,30) still widens to DECIMAL(38,30). Removed.

TLDR: DECIMAL(10,2) > DECIMAL(3,2) causes Hudi's validation to reject it when doing a dec > 1.00. Before the PR, the validation fails. First commit in the PR, it works by converting decimals to a common type. Intermediate PR changes that skipped decimal conversion caused it to fail again. With our changes at f24b3a3, it works again by restoring the decimal conversion.

Also in this commit:

  • A NULL operand widens with the numeric ones, matching the plan Spark builds for ts IN (1000, null) and ts <=> null.
  • Arithmetic and coalesce route through the same helper. ts + 1 > 1500 was rejected before.
  • Tests assert the validate half for every shape whose validation result this PR flips, since procedures validate before they filter. Previously all the new assertions went through evaluateFilter only, which skips that path.
  • TestFsViewProcedure now uses filter shapes master rejects, plus one that keeps no rows. The earlier data_file_size > 0 passed on master unchanged and expected the same row count as the unfiltered call, so it could not fail.

Spark 3.5.5 / 2.12 is green, 29 tests, scalastyle clean. The 4.1 / 2.13 suites need a re-run: they caught a DECIMAL(38,0) vs DECIMAL(38,18) assertion that 3.5 and 4.1 resolve differently (3.5 widens and overflows the cast, 4.1 declines to widen), which I dropped rather than pin to one version.

The per-row Try swallowed every failure into a dropped row. Once
the widening started casting operands, that hid a case the
equivalent query reports: under ANSI, an overflowing cast or
arithmetic raises SparkArithmeticException, and Hudi turned it
into a silently missing row. 10^30 > 1.5 is true, and the filter
excluded the row without telling the caller.

Let ArithmeticException out of both evaluateExpressionOnRow and
evaluateFilter, so Spark's own Cast semantics decide: null and a
dropped row without ANSI, a raised error with it. evaluateFilter
rethrows it unwrapped rather than restating it as a filter
expression problem, since the expression is fine and the data is
what does not fit. Other failures keep the existing handling.

Tests pin both ANSI modes for an overflowing Long addition, which
behaves the same on every supported Spark, and for the decimal
widening, which does not: Spark 3 widens DECIMAL(38,0) towards
DECIMAL(38,18) and overflows the clamp, while Spark 4 declines to
widen a lossy decimal pair in either ANSI mode and compares on
Decimal ordering instead.

@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 the thorough follow-up! This round hoists bind/resolve out of the per-row loop, routes all widening through a shared widenNumericOperands helper, and extends it to EqualNullSafe, In, BinaryArithmetic and Coalesce, with the ANSI/non-ANSI coercion object picked from SQLConf.get.

Checking the prior round: my EqualNullSafe question is addressed (it now goes through applyTypeCoercion, with ts <=> 1000 and ts <=> null pinned). @voonhous's points are all addressed too — the unresolved-operand guard (operands.exists(!_.resolved)) keeps dataType off unresolved nodes and preserves Or short-circuiting; ANSI mode selects AnsiTypeCoercion; In is wired via findWiderCommonType over value +: list; the no-op rebuild is avoided by the distinct.length == 1 bail-out; and the tests now use a multi-row fixture plus reversed-operand, negative and procedure-level (TestFsViewProcedure) coverage. The decimal-38 clamp is no longer bailed out but is documented on widenNumericOperands, and the resulting behavior matches what the analyzer would do for the same SQL comparison, so that seems a reasonable resolution.

One optional question on arithmetic coercion inline. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels Sep 7, 2026
Divide accepts only Double or Decimal, so widening its operands to
their common numeric type leaves an integral pair unresolved:
"ts / 2 > 500" was rejected as an unsupported filter expression
while "price / 2 > 5" worked, because price is already Double.
This predates the arithmetic widening -- Divide(Long, Int) was
unresolved for differing types before it too.

Mirror the analyzer's Division rule: promote an integral pair to
Double, and let a pair that already involves a decimal widen the
way the other arithmetic does. Divide has to be matched ahead of
the general BinaryArithmetic case, and the rebuild goes through
withNewChildren so the constructor difference between 3.3 and
3.4+ stays out of it.

Also correct two test comments. The overflowing Long addition
wraps to a negative without ANSI, it does not yield null; that is
a Cast behaviour, not an addition one. And the decimal overflow
test now pins only Spark 3, where the widening and the overflow
were both observed, instead of asserting a Spark 4 result whose
mechanism was inferred rather than measured.
Spark's Division rule guards on isNumericOrNull, "in case a query
contains null literals", so "ts / null > 0" and "null / ts > 0"
resolve there and evaluate to null. The Divide helper guarded on
NumericType alone, so both stayed unresolved and validation
rejected them, while widenNumericOperands already admitted
NullType. Share one isNumericOrNull between the two so the guards
cannot drift apart again.

The rule is unchanged between the majors this builds against, only
relocated from TypeCoercion.scala to DivisionTypeCoercion.scala;
link both from the helper. Link Spark's decimal precision rules
from findWiderNumericType the same way, and point the unsettled
decimal parity question at HUDI 19860.
BinaryArithmetic.checkInputDataTypes accepts two decimals of
different precision and scale, and Spark derives the result
precision from the operands, so widening them to a common type
does not enable the expression, it changes the answer.
DECIMAL(38,18) * DECIMAL(2,1) gives a scale-16 product that still
holds 0.0000001; casting both to DECIMAL(38,18) first drives the
product to scale 6 and rounds the value to zero, so
"dec * 1.0 > 0.0" dropped a row Spark keeps.

Leave any arithmetic with a decimal operand untouched. A decimal
against a non-decimal therefore stays unresolved and rejected,
which is what it was before this coercion existed, rather than
resolving to a wrong answer. Matching Spark there needs its
DecimalPrecision promotion, including the minimum-precision rule
for integral literals that exists to avoid this same loss; that
belongs with HUDI 19860.

Also fix a resolved check: the decimal guard reads dataType, which
throws on an unresolved operand.
validateFilterExpression never checked that the expression is
boolean, so a resolvable "ts + 1" now passed validation and
reported zero rows where Spark raises FILTER_NOT_BOOLEAN. Reject
any non-boolean result, string included, as Spark does.

The ANSI rethrow covered only ArithmeticException. An ANSI cast of
a malformed string raises SparkNumberFormatException or
SparkDateTimeException, which the per-row Try still swallowed into
a dropped row, so "int(name) > 1" returned nothing where the query
fails. Rethrow all three JDK types.

IntegralDivide accepts only Long or Decimal and its operands are
never widened against each other, so "id div 2" stayed unresolved
while "ts div 2" resolved. Mirror the analyzer's IntegralDivision
rule and promote each narrower integral operand to Long first.

A NullType operand now takes the type of a single non-numeric peer
too, so "name IN ('a1', null)" and "name <=> null" validate the
way Spark plans them. The helper is renamed widenOperands.

Drop the inner Try around bindAndResolveExpression: nothing
reaches its failure branch, and the outer IllegalArgumentException
is the better failure mode if something ever does.

Tests: pin the Spark 4 outcome of the decimal-overflow case (the
fractional operand is cast down, the row survives in both ANSI
modes) instead of asserting nothing there; cover the constant
folded by DecimalPrecision for a literal past the Long range, the
%, div and - operators and wider coalesce shapes, and a byte
literal (130) that a narrowing implementation would flip; add a
filter assertion to the show_metadata_column_stats_overlap test;
fold two duplicate decimal tests into their parity siblings; run
the retainFractionDigitsOnTruncate axis only where the key exists;
widen the TestFsViewProcedure empty control; rename scalarRow to
tsRow and alias java.math.BigDecimal.
Five tests imported scala.collection.JavaConverters._ locally. One
file-level import in the scala group replaces them, matching the
header layout of HoodieProcedureFilterUtils.
@voonhous

voonhous commented Sep 7, 2026

Copy link
Copy Markdown
Member

Pushed 1e8c689 and a6cc9da.

Validation now rejects a non-boolean filter the way Spark does (ts + 1 previously validated and returned zero rows). The ANSI rethrow also covers a malformed-string cast (int(name) > 1 raised in the query but returned nothing here). id div 2 resolves via Spark's IntegralDivision promotion, and a null operand takes a string peer's type (name IN ('a1', null)).

Tests pin the Spark 4 decimal-overflow outcome instead of skipping it, cover %, div, -, wider coalesce and the DecimalPrecision constant fold, and add a show_metadata_column_stats_overlap filter assertion for the second #19632 reproducer.

Verified on 3.3.4/3.5.5/4.1.1; the suite runs 26 tests after folding two duplicates.

@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

@hudi-bot

hudi-bot commented Sep 7, 2026

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 enabled auto-merge (squash) September 7, 2026 16:36
@voonhous
voonhous merged commit cebbd6a into apache:master Sep 7, 2026
22 of 25 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

5 participants