Skip to content

fix: read ARRAY<DOUBLE> through Arrow's Float8Vector - #69

Open
LuciferYang wants to merge 2 commits into
lance-format:mainfrom
LuciferYang:fix/converter-array-double-cast
Open

fix: read ARRAY<DOUBLE> through Arrow's Float8Vector#69
LuciferYang wants to merge 2 commits into
lance-format:mainfrom
LuciferYang:fix/converter-array-double-cast

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #68

Problem

The DoubleType branch of RowDataConverter.readArrayData cast the child vector to the private inner class Double8Vector, a wrapper that is never instantiated anywhere (its javadoc says "alias for Float8Vector"), a leftover from an unfinished refactor. Every read of an ARRAY<DOUBLE> column, via either Arrow representation that maps to it (List<Float64> and FixedSizeList<Float64>, i.e. Lance f64 vector columns), threw ClassCastException. The type layer accepts the schema (LanceTypeConverter documents FixedSizeList<Float64> <-> ARRAY<DOUBLE>), so DDL and planning succeed and the failure only surfaces at scan time. All read entry points (LanceSource, LanceInputFormat, LanceAggregateSource, LanceVectorSearch) funnel into this one private method and were all affected.

Fix

  • Cast to org.apache.arrow.vector.Float8Vector directly, matching the sibling FloatType branch and the write path.
  • Delete the dead Double8Vector wrapper.

Tests (RowDataConverterTest, 4 guards)

Test Behavior it pins
testWriteThenReadArrayOfDoubleRoundTrip List-representation write/read round trip: dense values, null element, empty array, null parent array
testReadFixedSizeListOfDouble FixedSizeList (f64 vector column) read: values, null element, null parent, start-index arithmetic
testWriteBeyondInitialListCapacity Writing 600 elements against a deliberately tiny initial child capacity forces repeated setSafe reallocation, the path the production sink hits on every 1024-row batch (default write.batch-size). The test asserts the small starting capacity so it cannot quietly become a no-op
testReadFixedSizeListOfFloat The sibling FloatType branch (f32 vector columns, the primary embedding path): values, null element, null parent, list size

Verification, on lance-flink-1.18 under JDK 17: RowDataConverterTest 4/4 green, and the full module unit suite is Tests run: 190, Failures: 0, Errors: 0, Skipped: 17. The 17 skips are LanceCatalogS3Test$MinioIntegrationTests, which need a MinIO endpoint and are unrelated to this change.

Six targeted mutations each reddened the guard that watches it, while the others stayed green: reverting the cast fix, an FSL start-index off-by-one, dropping the element null guard, dropping the parent-null short-circuit, setSafe -> set, and a wrong cast in the Float branch. Reverting the production fix reddens three of the four tests, not all four: testReadFixedSizeListOfFloat guards the sibling FloatType branch and stays green by design.

Build note

The new tests allocate off-heap Arrow memory, which requires --add-opens=java.base/java.nio=ALL-UNNAMED for MemoryUtil to initialize on JDK 17 (a CI matrix leg). The root pom now sets that argLine; jacoco appends its agent to the same property, so the two coexist.

readArrayData cast the child vector to the private inner Double8Vector
wrapper instead of org.apache.arrow.vector.Float8Vector. The wrapper is
never instantiated anywhere, so every read of an array-of-double column
(List or FixedSizeList, both map to ARRAY<DOUBLE>) threw
ClassCastException. The write path and the Float read branch already
used the Arrow vector directly; this was a leftover from an unfinished
refactor.

RowDataConverterTest covers both list representations for double and
float reads, plus empty/null arrays and a write batch that forces the
child vector to reallocate (the production sink path); each guard was
verified to redden under a targeted regression mutation. The tests
allocate off-heap memory, so the root pom also sets the
--add-opens=java.base/java.nio argLine that JDK 17 CI legs require for
Arrow's MemoryUtil to initialize.
Three things the guards did not actually establish.

The realloc test's comment had every number wrong. setInitialCapacity(4)
does not leave the child at 4: allocateNew rounds the 40-byte request to 64
and re-spreads it, landing on 7, and the child then grows 7 -> 15 -> 31 ->
63 -> 126 -> 252 -> 504 -> 1008, with the first realloc on row 2 rather than
row 1. The comment now says that, and the test asserts the starting capacity
is below 600 instead of taking it on faith. Without setInitialCapacity the
default is 4032, which swallows all 600 with no realloc at all, so the test
was one deleted line away from silently proving nothing; that line now
reddens the assertion.

setNull on a freshly allocated child is a no-op, since the validity bits
start at zero. Both FixedSizeList tests wrote a value first, so the null
slot sits over live data the way a real Arrow batch would, and row 2's own
slots carry values so a drifted read fails on 4.5 instead of finding a
conveniently null unwritten slot.

The float test was missing two assertions the double test had: the list
size, and a null parent row. It had no way to notice a broken parent-null
short-circuit; removing that short-circuit now reddens it.

Also asserts the middle element of each array in the realloc loop, which
previously checked only the first and last.
@LuciferYang
LuciferYang marked this pull request as ready for review August 22, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every read of an ARRAY<DOUBLE> column throws ClassCastException (cast to the never-instantiated inner Double8Vector)

1 participant