Skip to content

fix!: DH-23754: Reject lossy filter value coercion, read char integers as code points - #8726

Merged
lbooker42 merged 12 commits into
deephaven:mainfrom
lbooker42:engine/dh-23754-lossy-coercion
Oct 7, 2026
Merged

lbooker42 merged 12 commits into
deephaven:mainfrom
lbooker42:engine/dh-23754-lossy-coercion

Conversation

@lbooker42

@lbooker42 lbooker42 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

BREAKING CHANGE: MatchFilter and RangeFilter no longer truncate, wrap or saturate a filter value to fit the column's type. Each change below replaces a result that disagreed with the query language, that is, with the ConditionFilter these filters fail over to.

  • Now throws instead of selecting the wrong rows. These forms have no failover:
    • X in v with a lossy query-scope value. With v = 5.7 on an int column, main matched 5.
    • A lossy direct value. new MatchFilter(REGULAR, "X", 300) on a byte column matched 44 on main.
    • A lossy Filter API in or eq literal, such as FilterIn.of(X, 4, 5.5).
  • Now selects different rows:
    • X == v, X != v and range filters with a lossy query-scope value fail over to the query language. So X < v with v = 5.7 on an int column selects 4 and 5, where main selected 4.
    • On a char column, an unquoted integer literal is a code point: X == 5 is (char) 5. Write X == '5' for the digit.
    • A char compared with a numeric column is its code point, as in Java. The Filter API's lt(X, '5') on an int column compares with 53, where main used 5.

Split out of DH-23754. Rejecting values of the wrong type (checkValueTypes) is left to a separate PR, because it reverses the "Will not implement" decision on DH-21232.

The rule

MatchFilter and RangeFilter converted a query-scope value to the column's type with the query language's casts, which truncate, wrap and saturate. With v = 5.7 on an int column, X == v matched 5. With v = 300 on a byte column, it matched 44. The same condition written as a formula matches nothing.

A value must now convert exactly, meaning the converted value selects exactly the rows the query language would. Otherwise it is refused:

  • X == v, X != v and range filters fail over to their ConditionFilter, which is always correct.
  • X in v, direct values and Filter API values have no failover, so they throw.

Changes

  • Numeric columns. A single NumericColumnTypeConvertor handles byte through double. It casts the value (narrow), then checkRoundTrip compares the result with the original exactly, as BigDecimals. It also refuses:

    • a value that is not a number, which fails over as it did on main, where the cast threw ClassCastException;
    • a Number of a nonstandard type, such as DoubleAdder or AtomicLong, which has no exact value to check.
  • Chars. The query language, like Java, compares a char with a number by its code point. On numeric columns, byte through double, BigInteger and BigDecimal:

    • A Character query-scope or direct value converts to its code point, and so does a quoted char literal such as '5'.
    • A code point too large for a byte or short column is refused.
    • NULL_CHAR is null.

    On a char column, a number converts to the char with that code point, and NULL_CHAR (65535) is refused.

  • BigDecimal and BigInteger columns convert exactly. A floating-point value converts through BigDecimal.valueOf, as the query language compares it, and a long no longer goes through double. NaN converts to neither, so X == v fails over, where NaN equals no big number, and X in v throws. main matched 0 on a BigInteger column.

  • A BigDecimal or BigInteger on a float or double column. The query language compares these through BigDecimal.valueOf(double), which uses the column value's shortest decimal representation. So the value converts only if it equals that decimal for its converted value:

    • BigDecimal("0.1") converts to 0.1.
    • new BigDecimal(0.1) is refused. It is exactly a double, but no double's shortest decimal equals it.
    • BigDecimal("0.1") on a float column is refused, because 0.1f widens to 0.10000000149011612.
  • Direct values, such as new MatchFilter(options, "X", 5.7), now go through the same conversion. Before, they were used as given. Without nanMatch, NaN is now dropped from query-scope values on primitive columns, as it already was from direct values.

  • Range bounds. -0.0 against a byte, short, int or char column fails over. The query language compares these with Double.compare, which orders -0.0 below 0, so the converted bound 0 would select other rows. The RangeFilter javadoc documents this case, the remaining differences listed under "Not changed", and how null endpoints behave.

  • Values of another type. The convertors pass a value they do not convert through as it is, such as a String on a BigInteger column. It can never match, so MatchFilter now drops it from its values on every column type, not only BigDecimal. That stops a ClassCastException in the sorted-column pushdown on BigInteger, String, Instant and the other types it searches by ordering alone, and in the case-insensitive String filter. RangeFilter fails over for such a bound instead of casting it. Rejecting these values outright is left to the separate PR.

  • The Filter API. WhereFilterAdapter quotes a Character literal for a range filter, so FilterComparison.lt(X, Literal.of('5')) on a char column still means '5', not (char) 5.

Not changed

These knowingly differ from the query language:

  • A literal is read in the column's type. F == 0.1 on a float column means 0.1f, and X == -128 on a byte column means NULL_BYTE. The query language reads them as a double and an int. This is unchanged from main.
  • A value that converts exactly to the column's null value is null. An Integer -128 on a byte column, or a long Integer.MIN_VALUE on an int column, matches the null rows, as the literal -128 and the column type's own null value do, for ==, !=, in, direct values and range bounds. The query language compares it as a number below every value. This is unchanged from main. A char 65535 (NULL_CHAR) is the exception and is refused: it is the highest char, yet orders below every char.
  • Exact floating-point values on integral columns. A floating-point value that converts exactly to an int or long column, such as 2^24f or 2^53, matches only its exact equivalent. The query language's == also matches the integers that round to it.
  • A Float range bound on an int column converts to its exact int, and keeps the typed range filter. The query language compares the two in float, which rounds an int beyond 2^24, so X > 16777216f there excludes 16777217, which the converted bound includes. Failing over for every Float bound, even 5.0f, was too broad for a difference this rare.
  • Float and double range filters treat -0.0 and 0.0 as equal. The query language orders -0.0 below 0.0, so on rows holding -0.0, X < 0.0 and X >= 0.0 select other rows than it does.

Performance

A value that cannot be represented now costs a ConditionFilter, with no pushdown and no binary search, where main used a typed filter. Nearly every such query was answered wrongly on main. The exceptions are X == v on a double column with a long v beyond 2^53, where main's rounding to a double happens to agree with the query language's ==, and a range bound of another type, which threw ClassCastException on main.

Tests

  • MatchFilterCoercionTest checks each case against the ConditionFilter failover with assertSameRowsAsFailover, and pins the differences listed under "Not changed". It covers every primitive column type, BigInteger and BigDecimal columns, and char, null, NaN and -0.0 values, plus values of another type on sorted columns.
  • MatchFilterParamConversionTest covers the conversions themselves.
  • FailedOverFilterConsumersTest now forces a failover with 0x1p63 on a long column and new BigDecimal(1.1) on a double column.

🤖 Generated with Claude Code

@lbooker42 lbooker42 self-assigned this Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

No docs changes detected for 5c2a4e1

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Exact floating-to-integral conversions can still make optimized equality filters select different rows from the formula path.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Aligns optimized filters with query-language semantics by rejecting lossy coercions, treating integer char literals as code points, and supporting scale-insensitive BigDecimal matching.

Changes:

  • Adds exact numeric/char coercion with ConditionFilter failover.
  • Makes failed-over filters visible to pushdown and count-where consumers.
  • Implements ordering-based BigDecimal matching and updates binary-search kernels and tests.
File Description
Util/​.../​ObjectComparisons.java Clarifies equality versus ordering semantics.
replication/​.../​ReplicateRegionsAndRegionedSources.java Updates generated kernel documentation.
extensions/​parquet/​.../​ParquetTableFilterTest.java Tests scale-insensitive decimal pushdown.
engine/​table/​.../​ComparableRegionBinarySearchKernelTest.java Tests region dispatch behavior.
engine/​table/​.../​ComparableColumnBinarySearchKernelTest.java Updates equality-search documentation.
engine/​table/​.../​BinarySearchKernelHelperTest.java Registers BigDecimal ordering matching.
engine/​table/​.../​WhereFilterFactoryTest.java Updates failover API assertions.
engine/​table/​.../​MatchFilterParamConversionTest.java Tests parameter conversion boundaries.
engine/​table/​.../​MatchFilterCopyTest.java Tests copied and renamed failovers.
engine/​table/​.../​MatchFilterCoercionTest.java Covers coercion and char semantics.
engine/​table/​.../​BigDecimalMatchFilterTest.java Tests decimal matching paths.
engine/​table/​.../​QueryTableWhereTest.java Updates failover verification.
engine/​table/​.../​QueryTableWhereSpecialCasesTest.java Updates decimal match expectations.
engine/​table/​.../​FailedOverFilterConsumersTest.java Tests failover consumers.
engine/​table/​.../​DeferredViewTableTest.java Tests failover renaming.
engine/​table/​.../​BasePushdownFilterContextImplTest.java Updates pushdown failover assertions.
engine/​table/​.../​updateby/​countwhere/​CountWhereOperator.java Handles effective condition filters.
engine/​table/​.../​ShortRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​ShortColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​ObjectRegionBinarySearchKernel.java Supports ordering-based object bounds.
engine/​table/​.../​ObjectColumnBinarySearchKernel.java Supports ordering-based object bounds.
engine/​table/​.../​LongRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​LongColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​IntRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​IntColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​FloatRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​FloatColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​DoubleRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​DoubleColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​CharRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​CharColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​ByteRegionBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​ByteColumnBinarySearchKernel.java Uses ordering for inclusive bounds.
engine/​table/​.../​BinarySearchKernelHelper.java Adds BigDecimal fast-path eligibility.
engine/​table/​.../​WhereFilterAdapter.java Quotes character range literals.
engine/​table/​.../​RangeFilter.java Adds exact conversion and failover delegation.
engine/​table/​.../​MatchFilter.java Implements conversion and explicit failover state.
engine/​table/​.../​ConditionFilter.java Extracts effective condition filters.
engine/​table/​.../​ChunkMatchFilterFactory.java Dispatches decimal chunk matching.
engine/​table/​.../​BigDecimalChunkMatchFilterFactory.java Implements compare-based decimal matching.
engine/​table/​.../​by/​CountWhereOperator.java Handles effective condition filters.
engine/​table/​.../​BasePushdownFilterContextImpl.java Exposes failovers to pushdown.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java Outdated

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Character parameters still produce incorrect matches against BigDecimal and BigInteger columns.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Arbitrary Number subclasses can still be converted lossily through longValue().

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Lossy conversion lets nonstandard Number values bypass validation

engine/​table/​src/​main/​java/​io/​deephaven/​engine/​table/​impl/​select/​MatchFilter.java:486

Nonstandard Number implementations are treated as integers here, so lossy values can still pass conversion. For example, a DoubleAdder containing 5.7 has longValue() == 5; converting it for an int (or BigDecimal/BigInteger) column makes both sides compare as 5, so a direct MatchFilter matches 5 instead of rejecting the lossy value. Query-scope filters can also disagree with their formula failover. Please either reject unsupported Number subclasses or preserve and validate their fractional value rather than defaulting every non-float Number to longValue().

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

It changes core filtering semantics across conversion, pushdown, binary search, and stacked dependent work.

Review effort: Balanced
Findings: None

@lbooker42

Copy link
Copy Markdown
Contributor Author

Test coverage of changes vs e998cc69c

Files changed

File Lines Branches Changed lines
mod MatchFilter 378/462 (81%) 331/448 (73%) 123/125 (98%)
mod RangeFilter 133/166 (80%) 99/133 (74%) 18/22 (81%)
mod WhereFilterAdapter 116/149 (77%) 45/54 (83%) 1/1 (100%)

All new and modified files

0 new, 3 modified — lines 627/777 (80%), branches 475/635 (74%)

All new and modified lines

148 executable lines added or changed — 142/148 (95%) covered

Gaps

The one behavior this PR adds that no test drives is the range failover for an int column compared with a Float bound, which the query language compares in float; its conversion is exact, so only the explicit rule keeps it from selecting other rows, and nothing yet checks that rule against the failover. The rest is narrow: the unwrapping of a Python value, which these Java suites never pass; the error for an unquoted multi-letter literal on a char column; and, in RangeFilter, the array-value error and the two throws for a filter that cannot fail over, which count as changed only because conversionError was renamed.

Measured at f7193e961 against e998cc69c, the stack base (#8719 with #8709 merged in), so the numbers are this PR's own changes; #8709 and #8719 carry their own reports. Suites: test, testParallel, testSerial and testOutOfBand in :engine-table, and test and testOutOfBand in :extensions-parquet-table.

@lbooker42
lbooker42 force-pushed the engine/dh-23754-lossy-coercion branch from 41dde84 to 83b8752 Compare October 5, 2026 19:04
@lbooker42
lbooker42 force-pushed the engine/dh-23754-lossy-coercion branch from 83b8752 to 50e139f Compare October 5, 2026 20:26
lbooker42 and others added 5 commits October 5, 2026 14:29
… is the converted value's decimal

The query language compares a float or double column with a BigDecimal or BigInteger through
BigDecimal.valueOf(double). Such a value now converts when it equals the converted value's
shortest decimal, as BigDecimal("0.1") does for 0.1, so it keeps the typed filter and `in`
accepts it. Any other value still fails over, or throws for `in`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olumn

The query language, as Java, compares a char with a number by its code point. A Character
query-scope or direct value now converts that way for byte through double, BigInteger and
BigDecimal columns, so `X == c` keeps the typed filter, and `X in c`, a direct value and the
Filter API's eq(X, '5') no longer throw. A code point beyond a byte or short is rejected, so it
fails over, or throws where there is no failover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eric column

A single-quoted char literal ('5') on a byte through double, BigInteger or BigDecimal column
now converts to its code point, as the query language compares it. `X == '5'` and `X < '5'`
keep the typed filter, `X in '5'` matches 53, and the Filter API's lt(X, '5'), whose range
filter has no failover, no longer throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t column

The int-column-vs-Float failover was too broad: it failed over for every Float bound, even
5.0f. A Float bound now converts to its exact int, as a large floating-point value does for
MatchFilter. The query language compares the two in float, so a bound beyond 2^24 can order a
neighbouring int differently; that is an accepted difference, documented in RangeFilter and
pinned by floatBoundOnAnIntColumnIsExact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fail over for such a range bound

- MatchFilter drops any match value that is neither null nor an instance of the column type, not only on a
  BigDecimal column. Such a value matches nothing, but the sorted-column pushdown on BigInteger, String,
  Instant and the other ordering-only types, and the case-insensitive String filter, threw
  ClassCastException for it.
- NaN skips the convertor only on a primitive column, so the BigDecimal and BigInteger convertors reject it
  and the filter fails over, rather than keeping a Double among the values of a sorted big-number column.
- RangeFilter fails over for a bound the convertor left as it is, instead of casting it.
- Drop RangeFilter's dead column-reference guard and MatchFilter.ColumnTypeConvertor.isColumnReference, and
  unwrap a PyObject once in convertParam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

The implementation and coverage are consistent, with only a non-blocking test documentation inconsistency.

Review effort: Balanced
Findings: 1 Low severity

Open (1)

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Nonstandard non-finite numbers and leading-zero character literals still bypass the intended coercion semantics.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Reject non-standard numeric types before non-finite fast path

engine/​table/​src/​main/​java/​io/​deephaven/​engine/​table/​impl/​select/​MatchFilter.java:492

Non-standard Number instances containing NaN or infinity bypass the intended rejection because this branch returns before exactValue(a) validates the runtime type. For example, a DoubleAdder holding positive infinity narrows to Double.POSITIVE_INFINITY, compares equal here, and is accepted for a double column even though all nonstandard numeric types are documented as having no exact value. Validate the source type before the non-finite fast path so direct and in values are consistently refused.

Medium severity Match query language parsing for leading-zero char literals

engine/​table/​src/​main/​java/​io/​deephaven/​engine/​table/​impl/​select/​MatchFilter.java:814

This parses leading-zero literals differently from the query language. For example, X == 010 is compiled by the ConditionFilter path as Java's octal code point 8, but Long.parseLong("010") produces 10, so the typed char filter selects a different row—the exact mismatch this change is meant to avoid. Decode Java-style decimal/octal notation here (or fail over for leading-zero forms).

@lbooker42
lbooker42 marked this pull request as ready for review October 6, 2026 00:00
…ue is null

An Integer -128 on a byte column, a long Integer.MIN_VALUE on an int column and so on now match the null rows, as the
column type's own null value and the literal -128 do, for ==, !=, in, direct values and range bounds, instead of being
rejected. The query language compares such a value as a number, so these differ from the ConditionFilter, as the
literal already does. A char 65535 (NULL_CHAR) and a value that only wraps onto the null value are still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 00:23

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The breaking coercion changes span numerous numeric, null, character, and floating-point edge cases that merit final human validation.

Review effort: Balanced
Findings: None

@lbooker42
lbooker42 requested a review from cpwright October 6, 2026 00:34
…which MatchFilter now drops

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 01:30

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Nonstandard numeric values containing NaN or infinity can bypass the new rejection logic.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Nonstandard Number values bypass rejection for NaN or infinity

engine/​table/​src/​main/​java/​io/​deephaven/​engine/​table/​impl/​select/​MatchFilter.java:493

Nonstandard Number implementations bypass rejection when their doubleValue() is NaN or infinity. For example, a DoubleAdder containing POSITIVE_INFINITY reaches this branch, compares equal to the narrowed infinity, and is accepted even though exactValue rejects the same type for finite values. Validate non-floating operands through exactValue before returning from the non-finite branch so all nonstandard numbers are consistently refused.

Comment thread engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java Outdated
…es, document exactValue vs toBigDecimal, compare row sets with equals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 23:42

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The broad, breaking coercion changes affect filter execution, fallback, and storage pushdown across many numeric edge cases.

Review effort: Balanced
Findings: None

@lbooker42
lbooker42 enabled auto-merge (squash) October 6, 2026 23:48
@lbooker42
lbooker42 merged commit a35cfe2 into deephaven:main Oct 7, 2026
27 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants