Identity gets types: encoding ids, layout ids, and column names are now validated domain
primitives (EncodingId, LayoutId, ColumnName — sealed WellKnown/Custom shapes with a
total parse, strings only at the wire boundary), layout decode becomes pluggable through
LayoutDecoder/LayoutRegistry, and the reader gains compatibility with current Rust writers'
vortex.zoned zone-map id. Field names are strict on both sides of the file boundary — the
writer refuses what the reference toolchain cannot survive (a NUL-named column SIGABRTs its
Arrow FFI), the reader rejects duplicate/blank/control names loudly instead of corrupting
silently. Plus the dictionary code-scan lane lands in both fused compute kernels (~20×/~22×,
multi-leaf AND ~11×), and Chunk columns are one order-preserving typed map. Every wire-level
claim measured against the Rust (JNI) oracle; behavior divergences documented in
docs/compatibility.md.
Added
Compute.filteredSum(filterColumn, predicate, aggColumn)fuses a filter and a sum into a single scan — a row folds into the total only when the predicate selects it (a null filter row is excluded) and the aggregate value is non-null — with no intermediate selection bitmap. It matches a hand-written fused loop and is ~1.5× faster than the two-passfilter+sum. (57d2225b)Compute.filteredAggregate(chunk, filter, aggColumn)fuses a whole multi-columnRowFilter(an n-aryANDof column-bound predicate leaves) and folds the selected rows'SUM/MIN/MAX/non-null count over an aggregate column in a single pass — the multi-column counterpart offilteredSum, and the row-level kernel behind the Calcite boundary-chunk aggregate push-down. Anullaggregate column counts selected rows only (COUNT(*)). (2ba54888)core.model.ColumnName— the validated column-name domain primitive: non-blank, no control characters (the policy the writer, schema builder, and file parser all enforce —ColumnName.violationis the single source of truth). Field names are now strict on BOTH sides of the file boundary: the writer refuses blank/control names (IllegalArgumentException; NUL additionally crashes the reference toolchain's Arrow FFI), and the reader rejects files carrying them or duplicate field names (VortexException) — wire-legal is a floor, not a policy. Printable names of any shape ($$$$$, interior spaces, emoji) remain legal and round-trip against the reference implementation. (c993a355, dca815b9, d3b5b251)core.model.LayoutId— typed layout identity with the same sealed shape asEncodingId(WellKnownconstants plusCustom; layouts are runtime-pluggable in the reference implementation). The reader now recognizesvortex.zoned, the current canonical zone-map layout id in the Rust reference, alongside the legacyvortex.statsalias it keeps writing — files from current Rust writers scan and prune correctly. (7df3a0db)- Layout decode is pluggable:
LayoutDecoder+LayoutRegistry(reader.layout) mirror the encoding registry —LayoutRegistry.builder().registerDefaults().register(custom).build()passed to the newVortexReader.open(path, readRegistry, layoutRegistry)/VortexHttpReaderoverloads dispatches every layout decode, container children included, through the registry. Programmatic registration only (no service file); unknown layouts fail loudly. Zone-map pruning and filtered scans recognize the built-in layouts only. (fc488d04, dd196f17)
Changed
-
The CLI uber-jar is now fully self-contained for
vortex.zstdfiles: it bundles the FFM binding's nativelibzstdfor all six platforms (osx/linux/windows × x86_64/aarch64) viazstd-platform— previously it shipped the binding's classes but relied on a system libzstd. The library modules keep zstd optional. (1983656b) -
The zstd binding stays a two-artifact opt-in (
io.github.dfa1.zstd:zstd+zstd-platform, bothoptional; no JNI anywhere), and touchingvortex.zstdwithout it now fails with an actionableVortexExceptionnaming the two artifacts to add, instead of a rawNoClassDefFoundError. Default write/read paths never touch the binding (measured). (ae9f95cc) -
The little-endian
ValueLayoutconstants moved fromPTypeIO.LE_*toVortexFormat.LE_*— endianness is a property of the wire format, not of ptypes, andVortexFormatis where format facts live. Six classes that carried private copies (including reversed-name duplicates likeSHORT_LE) now share the single source; nothing outsideVortexFormatdefines awithOrder(LITTLE_ENDIAN)layout. (c060d34f) -
Chunk.columns()returns an order-preservingSequencedMap<ColumnName, Chunk.Column>— one map instead of two parallel string-keyed maps, with each column'sArrayandDTypetraveling together in theColumncarrier.column(String)stays as boundary sugar (plus acolumn(ColumnName)overload); iteration order is the schema/projection order, now guaranteed even for 1–2 column chunks. Typed names originate inScanIteratorfrom the file's already-certified schema. (f8ad15d1) -
Compute.filteredSumover a dictionary-encoded filter column is ~20× faster (best runs ~30×): the predicate is resolved against the dictionary's value pool once and the rawu8codes are scanned directly from their backing segments, instead of decoding every row through the per-element accessor — a fusedSUM(measure) WHERE category = …over 100M rows drops from ~760 ms to ~38 ms. (85e251cc) -
Compute.filteredAggregatetakes the same dictionary code-scan lane (COUNT(*)included) — ~22× faster on the same workload (~980 ms → ~46 ms over 100M rows), which the CalciteWHERE-filtered aggregate push-down inherits on its boundary chunks. (145791c7, 6e6d7dd0) -
A multi-column
ANDfilter no longer forfeits the dictionary lane: the dict-encoded leaf drives the code scan and the remaining predicates are evaluated only on its matches —SUM(…) WHERE category = 7 AND price > 500over 100M rows drops from ~2.3 s to ~200 ms (~11×). (12e13501) -
core.model.EncodingIdis now a sealed interface: the spec constants live in the nestedWellKnownenum (re-exported, soEncodingId.VORTEX_FOOcall sites compile unchanged) andCustomwraps any other wire string, which for the first time lets third-partyEncodingDecoder/EncodingEncoderimplementations declare ids outside the spec set.parseis total over non-blank ids — an unknown id yields a typedCustominstead of an emptyOptional. (ea88a91b) -
reader.decode.ArrayNodeis a single record carrying the typedEncodingId; theKnownArrayNode/UnknownArrayNodesplit and theArrayNode.offactory are gone. Decode dispatch, theallowUnknownpassthrough, and error messages are unchanged. A crafted file with a blank encoding id now fails asVortexExceptioninstead of escaping asIllegalArgumentException. (21810d7e) -
LayoutandZonedStatsSchemamoved to the newreader.layoutpackage, andLayout's misnamedString encodingIdcomponent is nowLayoutId layoutId. Unknown layouts still fail loudly, now with a typed id in the error. (7df3a0db, b08ace79) -
UnknownArray.encodingIdis a typedEncodingIdinstead of a raw string — aCustom, or aWellKnownwhose decoder is not registered. (7588aa31)