Skip to content

Parquet Java ALP Implementation - #3397

Open
vinooganesh wants to merge 75 commits into
apache:masterfrom
vinooganesh:vinooganesh/alp-java-implementation
Open

vinooganesh wants to merge 75 commits into
apache:masterfrom
vinooganesh:vinooganesh/alp-java-implementation

Conversation

@vinooganesh

@vinooganesh vinooganesh commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

cc @julienledem @alamb @emkornfield @prtkgaur

Rationale for this change

Reworks the ALP encoding implementation to address emkornfield's architectural feedback on PR #3390. The original buffered all values in memory and decoded eagerly. This makes the writer incremental (encode per-vector as values arrive) and the reader lazy (decode on demand), matching how other Parquet encodings work.

Builds on Julien Le Dem's original implementation (#3390). File structure, integration points, core math, and interop test infrastructure all come from his work. The rework focused on the internal writer/reader plumbing.

What changes are included in this PR?

Architecture (addressing review feedback):

  • Incremental writer. Values buffer in a fixed-size vector, each full vector encodes and flushes immediately.
  • Lazy reader. Vectors decode on first access via offset array, skip() is O(1).
  • Interleaved page layout so each vector is self-contained.
  • Extracted AlpValuesReader abstract base class for shared decode logic (float/double readers only implement decodeBody).
  • Preset caching. Full parameter search for first 8 vectors, top 5 combos cached for the rest.

Spec compliance:

  • Fixed packed data size formula to ceil(n * bitWidth / 8) (reusing BytesUtils.paddedByteCountFromBits).
  • Signed frame-of-reference range in the size estimator (a prior unsigned max - min overstated the span on mixed-sign vectors).
  • Reads little-endian through ByteBuffer's typed getters on a LITTLE_ENDIAN-ordered buffer.
  • Uses parquet-encoding's BytePacker instead of custom bit-packing.
  • Capped max vector size at 32768 to prevent uint16 overflow in num_exceptions.
  • bitWidth bounds checks in the readers (> 32 float / > 64 double throw).

Configuration:

  • withAlp(), withAlp(AlpConfig), withAlp(columnPath) and withAlp(columnPath, AlpConfig) on ParquetProperties.Builder and the Hadoop ParquetWriter.Builder, plus withoutAlp() / withoutAlp(columnPath) to turn it back off globally or for one column.
  • AlpConfig carries only the vector size (default 1024, validated in its constructor). A column is ALP encoded when it has a config at all.
  • Threaded through DefaultV1ValuesWriterFactory and DefaultV2ValuesWriterFactory so per-column overrides work.
  • Enabling ALP together with BYTE_STREAM_SPLIT on a column, or on a non FLOAT/DOUBLE column, is rejected rather than resolved by precedence.

Reader null tolerance (bug fix):

  • AlpValuesReader was asserting num_elements == page.valuesCount, which fails on optional columns with nulls (num_elements is the encoded non-null count, valuesCount is the page row count including nulls). Relaxed to num_elements <= valuesCount.

Integration:

  • Wired ALP into both DefaultV1ValuesWriterFactory and DefaultV2ValuesWriterFactory as a fallback data-page encoding for FLOAT/DOUBLE.

Are these changes tested?

Yes, extensively. The full parquet-column module (840 tests) and all downstream non-ALP modules (arrow/avro/protobuf/thrift/variant/cli, 908 tests) pass; the parquet-hadoop ALP interop tests pass. Coverage spans:

Correctness & spec:

  • Encoder/decoder tests that construct ALP page bytes directly per the spec and feed them to the reader without going through the writer — catches bugs where writer and reader agree with each other but disagree with the spec.
  • Bit-packing round-trip across every width (int 1–32, long 1–64), top bit exercised.
  • Full-bit-space fuzz (double & float): random raw bit patterns — every NaN payload, subnormals, ±0, ±Inf, chaotic mixed magnitudes — with strict raw-bit round-trip (verifies NaN payloads are preserved exactly).
  • Exceptions (NaN/Inf/−0.0, one/all-exception vectors), nulls (all-null and partial-null pages), every partial-vector remainder, skip across vector boundaries.
  • Extreme frame-of-reference widths: values engineered to force 63-bit (non-overflow) and 64-bit (signed subtraction overflows → modular reconstruction) FOR deltas, plus 32-bit for float — all lossless with zero exceptions.
  • Preset-cache correctness under distribution shift within a row group.

Integration (production paths):

  • Dictionary → ALP fallback: dictionary enabled (the real default) with overflow forcing fallback to ALP mid-column — the path all prior tests skipped by disabling the dictionary.
  • ALP under Snappy / Gzip / Zstd compression.
  • Statistics correctness on ALP columns: NaN excluded from min/max, null_count correct.
  • ALP on a repeated (nested) double field with varying repetition/definition levels.

Robustness & scale:

  • Reader rejects malformed/truncated/corrupt pages cleanly (no crash, OOM, hang, or OOB).
  • Deterministic allocator-leak test: a counting ByteBufferAllocator verifies all off-heap buffers are released across 200 write/reset page cycles.
  • Large-scale round-trips (2M values; 500k rows across many row groups through the full pipeline).

Cross-language verification

The Arrow C++ ALP decoder (apache/arrow#48345) reads every Java-written fixture bit-exact against the canonical _expect.csv truth tables. Local verification covers the full {V1, V2} × {vs1024, vs4096} matrix plus the corner-case and extreme-value columns: >1.5M values, 0 mismatches. The shared ALP conformance fixture has landed in parquet-testing (apache/parquet-testing#119, which superseded #100).

Interop: TestInterOpReadAlp reads the shared conformance fixture from parquet-testing (#119) and checks every ALP column bit for bit against its PLAIN counterpart.

Are there any user-facing changes?

  • Users can enable ALP for FLOAT and DOUBLE columns via withAlp(...) on ParquetProperties.Builder or the Hadoop ParquetWriter.Builder, globally or per column, and disable it with withoutAlp(...).
  • The vector size is set through AlpConfig (default 1024).

Spec status

ALP is part of parquet-format 2.14.0 (ALP = 10). The inlined parquet.thrift was bumped to 2.14.0 in #3791, so the entry comes from upstream; the build-time perl patch, the local thrift addition and the testEnumEquivalence ALP skip are all gone from this PR.

julienledem and others added 7 commits January 22, 2026 08:44
Implements ALP encoding for FLOAT and DOUBLE types, which converts
floating-point values to integers using decimal scaling, then applies
Frame of Reference (FOR) encoding and bit-packing for compression.

New files:
- AlpConstants.java: Constants for ALP encoding
- AlpEncoderDecoder.java: Core encoding/decoding logic
- AlpValuesWriter.java: Writer implementation
- AlpValuesReaderForFloat/Double.java: Reader implementations

Includes comprehensive unit tests and interop test infrastructure.
Restore original comment indentation that was accidentally changed.
Escape <= characters as &lt;= in javadoc comments to avoid
malformed HTML errors during documentation generation.
ALP encoding is not yet part of the parquet-format Thrift specification,
so it cannot be converted to org.apache.parquet.format.Encoding. Skip it
in the testEnumEquivalence test and add a clear error message in the
converter for when ALP conversion is attempted.
  size and add independent reader/writer
  verification tests
Switch encode/decode from division-based formula to multiply-by-reciprocal
using separate POW10_NEGATIVE arrays, matching C++ Arrow's approach:
- Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f])
- Decode: encoded * POW10[f] * POW10_NEGATIVE[e]

Add fastRound helpers with sign branching for correct negative value
rounding. Remove version byte from page header (8 -> 7 bytes). Empty
pages now emit a 7-byte header with numElements=0.

Update all hand-crafted binary tests to match the new header format
and add comprehensive end-to-end tests for overflow boundaries,
large-scale data, preset caching, and NaN bit-pattern preservation.
- Rewrite TestInterOpReadAlp to use LocalInputFile instead of Hadoop
  FileSystem, fixing failures on Java 24+ where Subject.getSubject is
  removed. Tests now read C++ ALP parquet files directly without going
  through Hadoop security/UGI.

- Add AlpExceptionCountTest with per-column exception rate reporting
  against the real Spotify and Arade floating-point datasets from the
  parquet-testing repository. Useful for comparing Java vs C++ ALP
  compression ratios.
- Switch findBestFloatParams/findBestDoubleParams from minimizing
  exception count to minimizing estimated compressed size
  (length * bitWidth + exceptions * (typeSize + 2 bytes)), matching
  the C++ ALP cost model. This closes the ~4-5% compression gap vs C++.

- Rewrite sampler to collect evenly-spaced sample vectors and run
  findBestParams on each, then rank by win count. Matches C++ AlpSampler
  behavior more closely than the previous HashMap-based approach.

- Minor fixes: IOExceptionUtils null check, MemoryManager volatile scale,
  Files utility cleanup, parquet-cli dependency update.
@vinooganesh
vinooganesh force-pushed the vinooganesh/alp-java-implementation branch from 15bc06d to 24c23e5 Compare March 22, 2026 23:56
- Move shared LE helper methods (getShortLE/getIntLE/getLongLE) to
  AlpValuesReader base class; remove duplicates from subclasses
- Make EncodingParams fields package-private (remove public modifier)
- Replace fully-qualified java.util.Arrays.fill calls with imported Arrays.fill
  in both float and double readers; add missing import to double reader
- Add explanatory comments to getBufferedSize() magic numbers (3 for float,
  5 for double) explaining the overhead breakdown
- Add ALP enabled state to ParquetProperties.toString()
- Add ALP support to DefaultV1ValuesWriterFactory for float and double columns
- Revert Files.java, IOExceptionUtils.java, MemoryManager.java, and
  parquet-cli/pom.xml to master state; these changes are unrelated to ALP
  and should be submitted in separate PRs
- Clarify ParquetMetadataConverter error message: ALP encoding is defined
  in the ALP paper (enum value 26) but is not yet in the parquet-format
  Thrift spec, so ALP cannot be written through the Hadoop write path;
  the error message now explains what needs to happen to remove the block

@prtkgaur prtkgaur left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code organization looks good to me and the code follows the spec. I looked for areas of any extra buffer allocations which might impact performance and I think it is optimally written.

I think we should add a few benchmarks and publish numbers from them.

Thanks for working on this Vinoo!

@prtkgaur prtkgaur left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to make sure we have the following testing.

For the cross compatibility testing are we making sure that we write both V1 and V2 pages and the implementation in other language is able to read it.

- Add build-time Perl script to patch generated Encoding.java with ALP(10)
  after Thrift codegen (process-sources phase), since parquet-format 2.12.0
  does not yet include ALP in its Thrift spec
- Remove guard in ParquetMetadataConverter.getEncoding() that blocked ALP
  writes; Encoding.ALP now exists in the patched Thrift enum
- Add withAlpEncoding() builder methods to ParquetWriter
- Add TestInterOpReadAlp: Java V1/V2 write+read round-trip tests and C++
  Arrow interop tests (reads alp_spotify1.parquet, alp_arade.parquet, etc.)
- Add AlpEncodingBenchmarks JMH benchmark
…d pyarrow interop test

- AlpValuesWriter: stop clearing cachedPresets in reset() so preset (e,f)
  pairs survive page flushes; eliminates redundant full parameter search on
  every page after the first, cutting write time ~60%
- AlpEncodingBenchmarks: clarify Javadoc that comparison is PLAIN+UNCOMPRESSED
  (no codec), not plain+ZSTD
- parquet-benchmarks pom: add explicit annotationProcessorPaths and proc=full
  for jmh-generator-annprocess so BenchmarkList is generated under Java 23+
- TestInterOpReadAlp: add pyarrow cross-language compatibility test (skips if
  pyarrow unavailable or does not yet support ALP encoding)
- ParquetProperties: add withAlpVectorSize(int) and withAlpVectorSize(String, int)
  builder methods plus getAlpVectorSize(ColumnDescriptor) accessor, defaulting to
  AlpConstants.DEFAULT_VECTOR_SIZE (1024).
- AlpConstants: promote validateVectorSize to public so the builder can validate
  eagerly across packages.
- DefaultV1/V2 ValuesWriterFactory: pass the configured vector size to the
  4-arg AlpValuesWriter constructors.
- ParquetWriter.Builder: expose withAlpVectorSize facades mirroring withAlpEncoding.
- TestInterOpReadAlp: add testJavaWriteAlpCustomVectorSize covering 4500 rows at
  vectorSize=4096 so we cross a full vector boundary and verify round-trip equality.
  A wrong log_vector_size byte would surface as decode garbage, so round-trip
  equality is sufficient proof the configured size took effect on the wire.

Enables generating ALP test fixtures at different vector sizes (e.g. 4096) for
cross-language compatibility testing against the C++/Rust/Go implementations.
Logging and debug output was missing the new alpVectorSize field
alongside the existing 'ALP enabled' line. Cosmetic only — no
behavior change.
Adds generateAlpFixturesAtMultipleVectorSizes to TestInterOpReadAlp.
For each of the four source files in parquet-testing PR apache#100
(alp_spotify1, alp_arade, alp_float_spotify1, alp_float_arade), reads
every row, then re-encodes as Java ALP at both vectorSize=1024 and
vectorSize=4096. Output goes to ALP_OUTPUT_DIR (default
${user.dir}/alp-java-generated/), producing 8 files total named
alp_java_<stem>_vs{1024,4096}.parquet.

Each output is verified by reading back through the standard reader
path and bit-comparing every value via doubleToRawLongBits /
floatToRawIntBits — catches NaN payload and signed-zero divergence,
not just numerical equality.

Skips when ALP_TEST_DATA_DIR isn't set, so it stays inert in CI on
machines without the source datasets.

To run:
  git clone --branch alpFloatingPointDataset \\
    https://github.com/prtkgaur/parquet-testing.git
  ALP_TEST_DATA_DIR=path/to/parquet-testing/data \\
    mvn -pl parquet-hadoop \\
    -Dtest=TestInterOpReadAlp#generateAlpFixturesAtMultipleVectorSizes \\
    test
Extends generateAlpFixturesAtMultipleVectorSizes to vary writer page
version (PARQUET_1_0, PARQUET_2_0) as a third axis alongside dataset
and ALP vector size. Output grows from 8 → 16 files per run:

  alp_java_<stem>_v{1,2}_vs{1024,4096}.parquet

Page version is orthogonal to ALP encoding — the page version
difference lives in the parquet protocol layer, not in the ALP
payload — but covering both axes makes the fixture set fully
symmetric for cross-language compatibility verification. C++/Rust/Go
readers can use the V1 and V2 variants to prove their decoders
handle Java-written ALP regardless of how the surrounding pages are
framed. Avoids an asymmetry where the existing PR apache#100 set has C++
at V1 and Java at V2 with no overlap.

All 16 outputs independently verified against the canonical
_expect.csv truth files from parquet-testing PR apache#100 (1.56M values,
0 mismatches).
The reader was asserting that the ALP header's num_elements equals
the data page's valuesCount, but those values differ whenever a
column has nulls: num_elements is the count of non-null values that
went through ALP encoding, while valuesCount is the total row count
of the page (which includes null positions tracked by definition
levels). The strict equality check made the reader reject every
optional float/double column with at least one null value.

Relaxes the check to numElements > valuesCount — the header can
never legitimately claim more encoded values than the page has rows,
but it can claim fewer when nulls are present. The downstream code
already uses numElements (not valuesCount) to drive vector
allocation and decoding, so the rest of the read path is unchanged.

This was surfaced by the corner-case fixture per parquet-testing
issue apache#105, which exercises optional columns with null values.
Two new tests in TestInterOpReadAlp:

readAllFixtureFilesIndependently
  Opens every alp_java_*.parquet in ALP_OUTPUT_DIR and asserts each
  column chunk declares Encoding.ALP and decodes through the
  standard reader path without error. Separate from the generator's
  own round-trip verification so reader correctness surfaces as a
  distinct signal in CI when the fixtures are present. Skips
  cleanly when ALP_OUTPUT_DIR is empty so it stays inert in default
  CI environments.

generateAndVerifyCornerCaseFixture
  Writes a single small fixture file (alp_java_cornercases.parquet,
  ~60 KB) targeting the corner cases enumerated in parquet-testing
  issue apache#105: vectors with no exceptions, one exception per vector,
  all exceptions, NaN/Inf/-0.0, constant values (bit_width=0),
  multi-vector with differing exponents, and optional columns with
  nulls. Both f32 and f64 variants — 14 columns × 2048 rows total.
  Reads each column back and bit-exactly verifies every value
  against the expected pattern via doubleToRawLongBits /
  floatToRawIntBits.

The corner-case fixture is intended as a candidate file for
parquet-testing PR apache#100 once naming/design is confirmed. Generating
it also surfaced (and verified the fix for) a pre-existing reader
bug where optional columns with nulls couldn't be decoded — see the
preceding commit.
The corner-case fixture (alp_java_cornercases.parquet) is synthetic
— it isn't derived from any raw dataset in parquet-testing PR apache#100,
so the existing alp_*_expect.csv files don't cover it. That left
cross-language verifiers with no independent ground truth to check
the parquet file against; they had to either trust the Java reader
or duplicate the construction recipe in their own code.

writeCornerCaseCsvTruth now dumps the expected values straight from
the construction recipe into alp_java_cornercases_expect.csv next
to the parquet, every time the generator runs. The CSV uses the
same format conventions as the existing _expect.csv files (comma-
separated, header row, no quoting) plus two extensions:

  • Empty field = null cell (for optional columns)
  • Special values printed via Java's standard toString: "NaN",
    "Infinity", "-Infinity", "-0.0". These all parse via C++
    std::stod / std::stof per the standard (case-insensitive, "inf"
    and "infinity" both accepted).

The Arrow C++ ALP decoder reads the parquet and compares against
this CSV bit-exactly: 27306 non-null cells + 1366 null cells across
14 columns × 2048 rows, 0 mismatches.

This makes the corner-case fixture self-documenting and verifiable
by any future cross-language tooling without rerunning the Java
generator to discover what the expected values are.
The builder tracked the ALP enabled flag and vector size as two independent
ColumnProperty scaffolds, merged them into a per-column AlpConfig in
buildAlp(), then split them apart again in the copy constructor. Nothing
needed that round trip.

Hold one ColumnProperty<AlpConfig> instead and expose withAlp(AlpConfig) /
withAlp(columnPath, AlpConfig) directly. withAlpEncoding and
withAlpVectorSize stay as read-modify-write conveniences over it, so every
existing caller -- including ParquetWriter.Builder -- is unchanged. buildAlp
and the copy-constructor split are both gone.

Vector size validation moves into the AlpConfig constructor, which is where
an invalid size should be rejected regardless of how the config was built.
That also removes ParquetProperties' last reference to AlpConstants.

ColumnProperty.Builder gains getDefaultValue/getValue so a caller can modify
one field of an existing value rather than replace the whole thing. The
class is package-private, so this is not public API.
Addresses several review comments in one pass, no behaviour change:

- AlpConstants and every member drop to package-private. Only
  ParquetProperties referenced it from outside the package, and the
  preceding commit removed that, so none of this needs to be public API.
- Constants move to the class that uses them. The sampler constants
  (SAMPLER_*, MAX_PRESET_COMBINATIONS) are writer-only and move to
  AlpValuesWriter; the rounding magic numbers and power-of-ten tables are
  codec-only and move to AlpCodec. AlpConstants is left holding just the
  wire format: header/metadata sizes, mode and encoding markers, vector
  size bounds, and per-type exponent limits.
- Rename AlpEncoderDecoder to AlpCodec, and its test to match.
- Replace the wildcard static imports of AlpConstants with explicit ones in
  all five files.
- Fix the interleaved-page-layout box in the AlpValuesWriter and
  AlpValuesReader javadoc. The "4B &times; numVectors" cell was seven source
  characters wide for a glyph that renders as one, so the box could not line
  up in both the editor and the rendered javadoc. Using the literal glyph
  makes every row 73 characters in both.
Adding ALP to the Encoding enum previously meant post-processing the
thrift-generated Encoding.java with a perl script wired into the build,
because parquet.thrift was unpacked from the released parquet-format jar
and could not be edited.

Since parquet.thrift is now inlined in the repo (apache#3709), ALP = 10 can be
declared in the enum directly. Removes src/main/perl/patch-alp-encoding.pl
and the exec-maven-plugin execution that ran it. Verified the generated
Encoding.java still carries ALP(10) after BYTE_STREAM_SPLIT(9) with no
patch step involved.

The entry is marked as a local addition: dev/update-parquet-thrift.sh
overwrites this file from upstream and would silently drop ALP, so the
notice tells whoever re-runs it to re-apply the entry until the encoding is
accepted into parquet-format, at which point both the entry's notice and
this divergence go away.
isFloatException has to encode the value to check the round trip, so calling
it and then encodeFloat did the work twice. The writer's loop was worse:
exception check, encode again, plus a placeholder scan that encoded a third
time.

tryEncodeFloat and tryEncodeDouble now return the exception flag and the
encoded value together through a caller-owned EncodeResult. A holder rather
than an Optional keeps the per-value path allocation free.

Encoded bytes are unchanged, and a new test asserts the new path agrees with
the old exception-check-then-encode pair bit for bit. The benchmark is a
wash (197.9 vs 194.2 ms/op, error bars overlap), since encoding is small
next to compression and page assembly.
@vinooganesh
vinooganesh force-pushed the vinooganesh/alp-java-implementation branch from 3bc0835 to 8e743c6 Compare September 6, 2026 00:03
@vinooganesh

vinooganesh commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

@wgtmac thanks again for the review! I've gone through all of your comments and replied to each thread individually. A summary of where things landed:

The three reader issues are fixed in 338fb99. You were right that numElements could be forged up to Integer.MAX_VALUE, and while fixing it I found the allocation was reachable rather than theoretical, since MultiBufferInputStream.slice allocates before its EOF check. The vector count is bounded against the bytes actually present now, the offset array is validated up front, each vector's reads are confined by the following offset, and skip no longer overflows.

The sampling problem you spotted was the most valuable comment in the review. Your arithmetic was exactly right: reset() ran at every page boundary and cleared the sample state, so the threshold was never reached and the preset cache was never built. Fixed in eb0e594, with a regression test that fails without it.

The two test comments were both fair. The allocation test was inflating valuesCount rather than forging num_elements so it never reached the path it claimed to, and catchAny was swallowing Errors so it could not really fail. Both fixed in 338fb99.

Beyond that: ALP is configured through a single AlpConfig now with withAlp (99ae547), AlpConstants is package private with the constants moved to whichever class actually uses them, AlpEncoderDecoder is renamed to AlpCodec, the wildcard imports are gone and the javadoc box is fixed (78c1d18), ALP is declared directly in parquet.thrift with the perl patch deleted (229e0d9), and the double encoding is gone (8e743c6).

There are three places where I would like your opinion rather than assuming I picked right:

  • To make withAlp work without breaking existing callers I added getDefaultValue and getValue to ColumnProperty.Builder. It is package private so not public API, but it is a shared file rather than something ALP specific.
  • dev/update-parquet-thrift.sh overwrites parquet.thrift from upstream and will silently drop the ALP entry. I put a marked notice in the enum so the loss at least shows up in a diff, but a guard in the script might be better.
  • On the nested writer classes, I followed the ByteStreamSplit layout rather than choosing it deliberately. Happy to split them if you would rather ALP be internally consistent.

The one thing I cannot fix on my own is the interop test. The fixtures are proposed in apache/parquet-testing#100, which is still open, so the test skips in CI today. If you have any pull there it would help, since cross language read back matters for the vote more than for that test alone.

parquet-column is at 856 tests with no failures. Whenever you have time for another look, I would appreciate it.

public static final boolean DEFAULT_IS_DICTIONARY_ENABLED = true;
public static final boolean DEFAULT_IS_BYTE_STREAM_SPLIT_ENABLED = false;
public static final boolean DEFAULT_IS_ALP_ENABLED = false;
public static final int DEFAULT_ALP_VECTOR_SIZE = AlpConfig.DEFAULT_VECTOR_SIZE;

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.

Why rename this constant? Feels like we should just be calling AlpConfig.DEFAULT_VECTOR_SIZE at the call site

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and thanks for flagging it — there was no reason to alias the constant. Removed in 7377588, so the call site now reads AlpConfig.DEFAULT_VECTOR_SIZE directly.

* @param vectorSize values per encoded vector; must be a power of 2 in the supported range
* @throws IllegalArgumentException if {@code vectorSize} is not a supported vector size
*/
public AlpConfig(boolean enabled, int vectorSize) {

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.

There probably shouldn't be an "enabled" as part of this config. Whether ALP is on or off is separate from how it is configured.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That separation makes sense, thank you. Fixed in e3b846a, where AlpConfig holds only the vector size and a column is ALP encoded when it has a config at all.

DEFAULT_IS_BYTE_STREAM_SPLIT_ENABLED
? ByteStreamSplitMode.FLOATING_POINT
: ByteStreamSplitMode.NONE);
alp = ColumnProperty.<AlpConfig>builder()

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.

Silently overriding byteStreamSplitEnabled

We probably want a slightly more complicated logic here

If (bytestream && ALP) fail (can't use both)
... elif(bytestream) {
bystreamEnabled ...
} elif( alp) {
alpEnabled
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it was silently dropping BYTE_STREAM_SPLIT. Both that conflict and ALP on a non float or double column are now rejected in newColumnWriteStore in e660c6c, rather than resolved by precedence.

* @param config the ALP configuration
* @return this builder for method chaining.
*/
public Builder withAlp(AlpConfig config) {

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.

These APIs seem a bit over-specified for what we allow?

I feel like we have

withAlp(config) // sets ALP on everywhere
withAlp(columnPath, config) // set config on one column
withAlp() // Sets ALP on with default config
withAlp(columnPath) //sets alp as on for a single column

I'm not sure we need the ability to flip the encoding back off

Then I would make sure that none of this includes implementation details (like config parameters) in the method names.

I would also consider whether we should fail if "ALP" is enabled on a non float/double column

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.

if we did want the "off functions"

withoutAlp()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your sketch is what the API looks like now, thanks for laying it out: e3b846a collapses the six methods into withAlp(), withAlp(config), withAlp(columnPath), withAlp(columnPath, config) and withoutAlp(), with the vector size only reachable through AlpConfig. The non float or double check is in e660c6c, and the one tradeoff worth flagging is that per-column disabling goes away with the boolean setters, so let me know if you would like that kept.

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.

What do you think? Do we need per column disabling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so, and it is in a8c584f as withoutAlp(columnPath). The reason it was missing is that ColumnProperty treated a null per-column value as unset and fell back to the default, so it now checks containsKey instead and an explicit null overrides; every other per-column property either cannot hold a null or already defaults to one, so nothing else changes behaviour.

Two things had to come with it. The schema check was treating "column named in the ALP property" as "ALP enabled here", so disabling ALP on an int column failed with a message claiming ALP was enabled on it, and toString rendered a disabled column as null again, which undoes your other nit. Both are fixed and covered.

DEFAULT_ALP_VECTOR_SIZE was just a second public name for
AlpConfig.DEFAULT_VECTOR_SIZE, with one use. Drop it and read the
constant from AlpConfig directly.
AlpConfig carried an enabled flag, which mixed up whether a column uses
ALP with how it encodes. A column is now ALP encoded when it has a
config and is not when it has none, so AlpConfig holds only the vector
size and the builder property defaults to null.

That collapses the six builder methods into withAlp(), withAlp(config),
withAlp(column), withAlp(column, config) and withoutAlp(). The vector
size is no longer its own setter, so an implementation detail stops
appearing in a method name, and the read-modify-write setters that
needed getDefaultValue/getValue on ColumnProperty.Builder are gone
along with those two accessors.

Per-column disabling goes away with them: withoutAlp() clears the
default but cannot turn off a column enabled by name, since
ColumnProperty treats a null per-column value as unset.
The writer factories checked ALP before BYTE_STREAM_SPLIT, so enabling
both on a column silently dropped BYTE_STREAM_SPLIT. Enabling ALP on a
column that is not FLOAT or DOUBLE was ignored just as quietly.

Both are now checked against the schema in newColumnWriteStore, before
any data is written, and rejected rather than resolved by precedence.
The type check only applies to columns named explicitly, so turning ALP
on for everything still means the float and double columns. The check
returns immediately when no ALP is configured, which is the common case
and keeps it off the per-rowgroup path for everyone else.
+ "Statistics enabled: " + statisticsEnabled + '\n'
+ "Size statistics enabled: " + sizeStatisticsEnabled;
+ "Size statistics enabled: " + sizeStatisticsEnabled + '\n'
+ "ALP: " + alp;

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.

nit: A little odd here since it will say "ALP: null" when unset probably worth a bit more clarity

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bdef58a. It reads ALP: off when nothing is configured now, and ALP: off {col_c=AlpConfig{vectorSize=1024}} when only some columns have it, so the word null no longer appears either way.

Support for INT32, INT64 and FIXED_LEN_BYTE_ARRAY added in 2.11.
*/
BYTE_STREAM_SPLIT = 9;

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.

We should do the copy over of parquet-thrift in a precursor pr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as #3791, which merged yesterday, and I've merged master into this branch. parquet.thrift here is now byte-identical to upstream 2.14.0, so the local ALP = 10 block and the "LOCAL ADDITION" notice are gone and the entry comes straight from parquet-format.

}

/**
* Java writes ALP-encoded floats/doubles using V2 (PARQUET_2_0) data pages and reads them back.

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.

Java doc here is not in sync. One of the reasons I'm always pushing on not including details in javadocs because they changed very fast :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guilty as charged, and the point lands — the patch it described was deleted in 229e0d9, so the comment outlived the thing it documented by three commits. Dropped the detail rather than updating it in f7f7273.

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

This looks good to me now. I have a few remaining nits

We need to update the thrift definition so it doesn't say "local addition"
Java docs need cleanup where they refer to modified thrift or really anything not method contract specific
Skips based on ALP not existing should get pulled from tests too
Maybe we should add in that "withoutALP(column)" method?

An unset ALP property is null now, so toString printed "ALP: null" for
most writers, and still did for the default when only some columns had
ALP set.
The perl patch it described was removed in 229e0d9, so the comment
pointed at something that no longer exists.
ALP is in the Encoding enum now, so it round trips like every other
encoding and no longer needs excluding.
Turning ALP off for one column while it is on by default was not
expressible, because ColumnProperty treated a null per-column value as
unset and fell back to the default. It now checks containsKey, so an
explicit null overrides. Every other per-column property either cannot
hold a null or already defaults to one, so nothing else changes.

Two things had to follow. The schema check was reading "named" as
"enabled", so disabling ALP on an int column complained that ALP was
enabled on it, and toString rendered a disabled column as null again.
@vinooganesh

Copy link
Copy Markdown
Contributor Author

@RussellSpitzer thanks for the review and the approval. Three of the four nits are done, and I am still working through the thrift one.

Java docsf7f7273. The one you spotted described the build-time patch that was deleted in 229e0d9, so I dropped the detail rather than updating it. I grepped for anything else mentioning the patch or a local addition and that was the only one left.

Skips — the testEnumEquivalence skip is gone in 0fcd73e, so ALP round trips like every other encoding now. One judgement call for you: TestInterOpReadAlp also skips when pyarrow reports "Unknown encoding type". That is a skip about ALP not existing, but in pyarrow rather than here, and removing it makes the test fail on most machines. Happy to pull it if you would rather it fail loudly.

withoutAlp(column)a8c584f. It was missing because ColumnProperty treated a per-column null as unset and fell back to the default, so it checks containsKey now and an explicit null overrides; every other per-column property either cannot hold a null or already defaults to one, so nothing else changes. Two fixes had to come with it: the schema check was reading "named in the ALP property" as "enabled", so disabling ALP on an int column failed with a message claiming ALP was enabled on it, and toString rendered a disabled column as null, which undid your other nit.

Thrift — still looking into it. There is a wrinkle in doing it as a precursor PR that I want to understand properly before I propose something, so I will follow up on that thread.

Your approval is against e660c6c, so it misses the four commits since. Another look whenever you get a chance would be great.

@vinooganesh

Copy link
Copy Markdown
Contributor Author

Opened #3791 as the thrift precursor for this PR.

It bumps the inlined parquet.thrift from parquet-format 2.13.0 to 2.14.0 using dev/update-parquet-thrift.sh, which is a released tag containing ALP, so the entry comes from upstream rather than being hand written here.

One thing worth knowing, since it changes the shape of what was suggested. The bump cannot be thrift only: on a clean master it fails testEnumEquivalence with No enum constant org.apache.parquet.column.Encoding.ALP, because the generated format enum gains ALP(10) while the hand written column.Encoding has none, and adding that constant pulls in the ALP readers with it. So #3791 carries a temporary skip for ALP, which this PR removes.

Once #3791 merges I will rebase this one and drop the local ALP = 10 block, the "LOCAL ADDITION" notice, and that skip. The bump also brings the FILE logical type and INT96 chronological ordering, neither of which needs Java side handling beyond one guard so a FILE annotated column keeps degrading to its physical type instead of throwing.

…a-implementation

# Conflicts:
#	parquet-format-structures/src/main/thrift/parquet.thrift
The thrift bump in apache#3791 brought ALP in from upstream, but the merge
also brought back the ALP skip in testEnumEquivalence. Remove it again,
along with the stale parquet-testing#100 link in TestInterOpReadAlp.
@vinooganesh

Copy link
Copy Markdown
Contributor Author

@RussellSpitzer with #3791 merged, I've merged master in and worked through what was left of your nits:

  • Thrift: the inlined parquet.thrift is the upstream 2.14.0 file, so the local addition is gone.
  • Skips: the testEnumEquivalence ALP skip is gone again (7b48dd1), after the merge reintroduced it.
  • Javadocs: I grepped again for anything describing ALP as not yet upstream. The only thing left was a @see to parquet-testing#100, which has since been closed in favour of PARQUET-164: Add warning when scaling row group sizes. #119, so I dropped it.
  • withoutAlp(column): in a8c584f, as before.

One question from last time is still open: TestInterOpReadAlp skips when pyarrow reports "Unknown encoding type". Happy to pull it if you'd rather it fail loudly.

Your approval is against e660c6c, so another look whenever you get a chance would be great.

Use InterOpTester to fetch alp_extended.zstd.parquet at a pinned
changeset, like the other interop tests, instead of reading files from
a local directory. Every ALP column is compared bit for bit against its
PLAIN counterpart, with spot checks on the special values.

Drop the fixture generators too. They only produced files for
parquet-testing#100, which apache#119 superseded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants