This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new b25bf1ed8 GH-3530: Optimize PLAIN with direct ByteBuffer I/O (#3565)
b25bf1ed8 is described below

commit b25bf1ed8f9b06e18454f781a18e921546cd3d9b
Author: Ismaël Mejía <[email protected]>
AuthorDate: Sat Jul 11 22:47:35 2026 +0200

    GH-3530: Optimize PLAIN with direct ByteBuffer I/O (#3565)
    
    * GH-3530: Optimize PLAIN encoding and decoding with direct ByteBuffer I/O
    
    Replace ByteBufferInputStream and LittleEndianDataInputStream wrappers
    with direct ByteBuffer access for all PLAIN value readers and writers.
    
    Readers (PlainValuesReader, BooleanPlainValuesReader,
    BinaryPlainValuesReader, FixedLenByteArrayPlainValuesReader) now hold a
    little-endian ByteBuffer obtained from initFromPage() and call
    getInt/getLong/getFloat/getDouble directly, eliminating per-value stream
    overhead.
    
    Writers (PlainValuesWriter, BooleanPlainValuesWriter,
    FixedLenByteArrayPlainValuesWriter) write through
    CapacityByteArrayOutputStream's new writeInt/writeLong methods, which
    put values directly into the NIO slab buffer in little-endian order,
    avoiding temporary byte-array allocation.
    
    Supporting changes:
    - CapacityByteArrayOutputStream: allocate slabs with 
ByteOrder.LITTLE_ENDIAN,
      add writeInt(int) and writeLong(long) for single-value NIO writes.
    - BytesInput: add zero-copy writeTo(ByteBuffer) and toByteArray() using
      bulk ByteBuffer.get() instead of stream copy.
    - LittleEndianDataOutputStream: batch single-byte writes into single
      write(buf, 0, N) calls for writeShort/writeInt.
    
    Includes JMH benchmarks (PlainEncodingBenchmark, PlainDecodingBenchmark)
    covering all 7 primitive types for both encoding and decoding.
    
    * GH-3530: Address review comments on PLAIN encoding readers
    
    - Wrap RuntimeException in ParquetDecodingException in all read/skip
      methods to preserve existing error contract
    - Extract EMPTY_LE_BUFFER constant for empty page initialization
    - Add bounds check in BooleanPlainValuesReader.readBoolean()
    - Use Math.multiplyExact in skip(int n) to detect overflow
    
    * GH-3530: Migrate BytesInput internal calls away from deprecated 
toByteArray/toByteBuffer
    
    - Base-class toInputStream() now tries getInternalByteBuffer() first
      for zero-copy path before falling back to deprecated toByteBuffer()
    - ByteArrayBytesInput: add getInternalByteBuffer() and toInputStream()
      overrides to avoid unnecessary copy through BAOS
    - Add @SuppressWarnings("deprecation") on intentional deprecated overrides
---
 parquet-benchmarks/pom.xml                         |   9 +
 .../parquet/benchmarks/PlainDecodingBenchmark.java | 289 +++++++++++++++++++++
 .../parquet/benchmarks/PlainEncodingBenchmark.java | 257 ++++++++++++++++++
 .../apache/parquet/benchmarks/TestDataFactory.java | 108 ++++++++
 .../values/plain/BinaryPlainValuesReader.java      |  48 ++--
 .../values/plain/BooleanPlainValuesReader.java     |  75 +++---
 .../values/plain/BooleanPlainValuesWriter.java     |  46 +++-
 .../plain/FixedLenByteArrayPlainValuesReader.java  |  35 ++-
 .../plain/FixedLenByteArrayPlainValuesWriter.java  |  16 +-
 .../column/values/plain/PlainValuesReader.java     |  67 ++---
 .../column/values/plain/PlainValuesWriter.java     |  43 +--
 .../plain/TestBinaryPlainValuesWriterReader.java   | 132 ++++++++++
 .../plain/TestBooleanPlainValuesWriterReader.java  | 141 ++++++++++
 ...stFixedLenByteArrayPlainValuesWriterReader.java | 168 ++++++++++++
 .../values/plain/TestPlainValuesWriterReader.java  | 262 +++++++++++++++++++
 .../java/org/apache/parquet/bytes/BytesInput.java  |  64 ++++-
 .../bytes/CapacityByteArrayOutputStream.java       |  30 +++
 .../bytes/LittleEndianDataOutputStream.java        |  20 +-
 .../org/apache/parquet/bytes/TestBytesInput.java   |  23 ++
 .../bytes/TestCapacityByteArrayOutputStream.java   |  55 ++++
 .../hadoop/TestColumnChunkPageWriteStore.java      |  11 +-
 pom.xml                                            |   2 +
 22 files changed, 1732 insertions(+), 169 deletions(-)

diff --git a/parquet-benchmarks/pom.xml b/parquet-benchmarks/pom.xml
index d5a288b67..3ce4dce5c 100644
--- a/parquet-benchmarks/pom.xml
+++ b/parquet-benchmarks/pom.xml
@@ -94,6 +94,15 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>org.openjdk.jmh</groupId>
+              <artifactId>jmh-generator-annprocess</artifactId>
+              <version>${jmh.version}</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git 
a/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainDecodingBenchmark.java
 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainDecodingBenchmark.java
new file mode 100644
index 000000000..59a67c90c
--- /dev/null
+++ 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainDecodingBenchmark.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.benchmarks;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.column.values.plain.BinaryPlainValuesReader;
+import org.apache.parquet.column.values.plain.BooleanPlainValuesReader;
+import org.apache.parquet.column.values.plain.BooleanPlainValuesWriter;
+import 
org.apache.parquet.column.values.plain.FixedLenByteArrayPlainValuesReader;
+import 
org.apache.parquet.column.values.plain.FixedLenByteArrayPlainValuesWriter;
+import org.apache.parquet.column.values.plain.PlainValuesReader;
+import org.apache.parquet.column.values.plain.PlainValuesWriter;
+import org.apache.parquet.io.api.Binary;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OperationsPerInvocation;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Decoding micro-benchmarks for the PLAIN encoding across all Parquet 
primitive types:
+ * {@code BOOLEAN}, {@code INT32}, {@code INT64}, {@code FLOAT}, {@code 
DOUBLE},
+ * {@code BINARY}, and {@code FIXED_LEN_BYTE_ARRAY}.
+ *
+ * <p>Each invocation decodes {@value #VALUE_COUNT} values. Per-value methods 
measure
+ * scalar read throughput using direct ByteBuffer access.
+ *
+ * <p>Fixed-width types (BOOLEAN through DOUBLE) are data-independent for PLAIN
+ * decoding -- the cost per value is constant regardless of the value content 
or
+ * distribution pattern -- so no {@code @Param} is needed.
+ *
+ * <p>Variable/fixed-length byte types use inner {@link State} classes with
+ * {@code @Param} for the dimension that genuinely affects PLAIN throughput:
+ * <ul>
+ *   <li><b>BINARY:</b> parameterized by {@link BinaryState#stringLength} (10, 
100,
+ *       1000) because PLAIN reads a 4-byte length prefix then slices N content
+ *       bytes.</li>
+ *   <li><b>FIXED_LEN_BYTE_ARRAY:</b> parameterized by {@link 
FlbaState#fixedLength}
+ *       (2, 12, 16) because PLAIN slices exactly N bytes per value, covering
+ *       FLOAT16, INT96/legacy-timestamp, and UUID sizes.</li>
+ * </ul>
+ */
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class PlainDecodingBenchmark {
+
+  static final int VALUE_COUNT = 100_000;
+  private static final int INIT_SLAB_SIZE = 64 * 1024;
+  private static final int PAGE_SIZE = 4 * 1024 * 1024;
+
+  // ---- Pre-encoded pages for fixed-width types ----
+
+  private byte[] boolPage;
+  private byte[] intPage;
+  private byte[] longPage;
+  private byte[] floatPage;
+  private byte[] doublePage;
+
+  @Setup(Level.Trial)
+  public void setup() throws IOException {
+    Random r = new Random(42);
+
+    // Encode BOOLEAN
+    {
+      BooleanPlainValuesWriter w = new BooleanPlainValuesWriter();
+      for (int i = 0; i < VALUE_COUNT; i++) {
+        w.writeBoolean(r.nextBoolean());
+      }
+      boolPage = w.getBytes().toByteArray();
+      w.close();
+    }
+
+    // Encode INT32
+    {
+      PlainValuesWriter w = new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, 
new HeapByteBufferAllocator());
+      for (int i = 0; i < VALUE_COUNT; i++) {
+        w.writeInteger(r.nextInt());
+      }
+      intPage = w.getBytes().toByteArray();
+      w.close();
+    }
+
+    // Encode INT64
+    {
+      PlainValuesWriter w = new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, 
new HeapByteBufferAllocator());
+      for (int i = 0; i < VALUE_COUNT; i++) {
+        w.writeLong(r.nextLong());
+      }
+      longPage = w.getBytes().toByteArray();
+      w.close();
+    }
+
+    // Encode FLOAT
+    {
+      PlainValuesWriter w = new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, 
new HeapByteBufferAllocator());
+      for (int i = 0; i < VALUE_COUNT; i++) {
+        w.writeFloat(r.nextFloat());
+      }
+      floatPage = w.getBytes().toByteArray();
+      w.close();
+    }
+
+    // Encode DOUBLE
+    {
+      PlainValuesWriter w = new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, 
new HeapByteBufferAllocator());
+      for (int i = 0; i < VALUE_COUNT; i++) {
+        w.writeDouble(r.nextDouble());
+      }
+      doublePage = w.getBytes().toByteArray();
+      w.close();
+    }
+  }
+
+  // ---- BINARY state: parameterized by string length ----
+
+  /**
+   * Separate state for BINARY decode benchmarks. Pre-encodes a page of binary 
values
+   * so the {@code stringLength} parameter only creates trials for 
binary-related
+   * benchmark methods.
+   */
+  @State(Scope.Thread)
+  public static class BinaryState {
+    /** Short (10), medium (100), and long (1000) strings. */
+    @Param({"10", "100", "1000"})
+    public int stringLength;
+
+    byte[] page;
+
+    @Setup(Level.Trial)
+    public void setup() throws IOException {
+      Binary[] data =
+          TestDataFactory.generateBinaryData(VALUE_COUNT, stringLength, 0, 
TestDataFactory.DEFAULT_SEED);
+      PlainValuesWriter w = new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, 
new HeapByteBufferAllocator());
+      for (Binary v : data) {
+        w.writeBytes(v);
+      }
+      page = w.getBytes().toByteArray();
+      w.close();
+    }
+  }
+
+  // ---- FLBA state: parameterized by fixed byte length ----
+
+  /**
+   * Separate state for FIXED_LEN_BYTE_ARRAY decode benchmarks. Pre-encodes a 
page of
+   * FLBA values so the {@code fixedLength} parameter only creates trials for
+   * FLBA-related benchmark methods. Values: 2 = FLOAT16, 12 = INT96, 16 = 
UUID.
+   */
+  @State(Scope.Thread)
+  public static class FlbaState {
+    /** FLOAT16 (2), INT96 (12), UUID (16). */
+    @Param({"2", "12", "16"})
+    public int fixedLength;
+
+    byte[] page;
+
+    @Setup(Level.Trial)
+    public void setup() throws IOException {
+      Binary[] data = TestDataFactory.generateFixedLenByteArrays(
+          VALUE_COUNT, fixedLength, 0, TestDataFactory.DEFAULT_SEED);
+      FixedLenByteArrayPlainValuesWriter w = new 
FixedLenByteArrayPlainValuesWriter(
+          fixedLength, INIT_SLAB_SIZE, PAGE_SIZE, new 
HeapByteBufferAllocator());
+      for (Binary v : data) {
+        w.writeBytes(v);
+      }
+      page = w.getBytes().toByteArray();
+      w.close();
+    }
+  }
+
+  // ---- BOOLEAN ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeBoolean(Blackhole bh) throws IOException {
+    ValuesReader reader = new BooleanPlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(boolPage)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readBoolean());
+    }
+  }
+
+  // ---- INT32 ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeInt(Blackhole bh) throws IOException {
+    PlainValuesReader.IntegerPlainValuesReader reader = new 
PlainValuesReader.IntegerPlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(intPage)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readInteger());
+    }
+  }
+
+  // ---- INT64 ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeLong(Blackhole bh) throws IOException {
+    PlainValuesReader.LongPlainValuesReader reader = new 
PlainValuesReader.LongPlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(longPage)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readLong());
+    }
+  }
+
+  // ---- FLOAT ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeFloat(Blackhole bh) throws IOException {
+    PlainValuesReader.FloatPlainValuesReader reader = new 
PlainValuesReader.FloatPlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(floatPage)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readFloat());
+    }
+  }
+
+  // ---- DOUBLE ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeDouble(Blackhole bh) throws IOException {
+    PlainValuesReader.DoublePlainValuesReader reader = new 
PlainValuesReader.DoublePlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(doublePage)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readDouble());
+    }
+  }
+
+  // ---- BINARY (parameterized by string length) ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeBinary(BinaryState state, Blackhole bh) throws IOException 
{
+    BinaryPlainValuesReader reader = new BinaryPlainValuesReader();
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(state.page)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readBytes());
+    }
+  }
+
+  // ---- FIXED_LEN_BYTE_ARRAY (parameterized by fixed length) ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public void decodeFixedLenByteArray(FlbaState state, Blackhole bh) throws 
IOException {
+    FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(state.fixedLength);
+    reader.initFromPage(VALUE_COUNT, 
ByteBufferInputStream.wrap(ByteBuffer.wrap(state.page)));
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      bh.consume(reader.readBytes());
+    }
+  }
+}
diff --git 
a/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainEncodingBenchmark.java
 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainEncodingBenchmark.java
new file mode 100644
index 000000000..71d8e6008
--- /dev/null
+++ 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/PlainEncodingBenchmark.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.benchmarks;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.column.values.ValuesWriter;
+import org.apache.parquet.column.values.plain.BooleanPlainValuesWriter;
+import 
org.apache.parquet.column.values.plain.FixedLenByteArrayPlainValuesWriter;
+import org.apache.parquet.column.values.plain.PlainValuesWriter;
+import org.apache.parquet.io.api.Binary;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OperationsPerInvocation;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Encoding micro-benchmarks for the PLAIN encoding across all Parquet 
primitive types:
+ * {@code BOOLEAN}, {@code INT32}, {@code INT64}, {@code FLOAT}, {@code 
DOUBLE},
+ * {@code BINARY}, and {@code FIXED_LEN_BYTE_ARRAY}.
+ *
+ * <p>Measures per-value scalar write throughput using direct ByteBuffer I/O
+ * through {@code CapacityByteArrayOutputStream}.
+ *
+ * <p>Fixed-width types (BOOLEAN through DOUBLE) are data-independent for PLAIN
+ * encoding -- the cost per value is constant regardless of the value content 
or
+ * distribution pattern -- so no {@code @Param} is needed.
+ *
+ * <p>Variable/fixed-length byte types use inner {@link State} classes with
+ * {@code @Param} for the dimension that genuinely affects PLAIN throughput:
+ * <ul>
+ *   <li><b>BINARY:</b> parameterized by {@link BinaryState#stringLength} (10, 
100,
+ *       1000) because PLAIN writes a 4-byte length prefix + N content 
bytes.</li>
+ *   <li><b>FIXED_LEN_BYTE_ARRAY:</b> parameterized by {@link 
FlbaState#fixedLength}
+ *       (2, 12, 16) because PLAIN writes exactly N bytes per value, covering
+ *       FLOAT16, INT96/legacy-timestamp, and UUID sizes.</li>
+ * </ul>
+ */
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class PlainEncodingBenchmark {
+
+  static final int VALUE_COUNT = 100_000;
+  private static final int INIT_SLAB_SIZE = 64 * 1024;
+  private static final int PAGE_SIZE = 4 * 1024 * 1024;
+
+  // ---- Fixed-width data (no @Param needed -- PLAIN cost is constant per 
value) ----
+
+  private boolean[] boolData;
+  private int[] intData;
+  private long[] longData;
+  private float[] floatData;
+  private double[] doubleData;
+
+  @Setup(Level.Trial)
+  public void setup() {
+    Random r = new Random(42);
+    boolData = new boolean[VALUE_COUNT];
+    intData = new int[VALUE_COUNT];
+    longData = new long[VALUE_COUNT];
+    floatData = new float[VALUE_COUNT];
+    doubleData = new double[VALUE_COUNT];
+    for (int i = 0; i < VALUE_COUNT; i++) {
+      boolData[i] = r.nextBoolean();
+      intData[i] = r.nextInt();
+      longData[i] = r.nextLong();
+      floatData[i] = r.nextFloat();
+      doubleData[i] = r.nextDouble();
+    }
+  }
+
+  // ---- BINARY state: parameterized by string length ----
+
+  /**
+   * Separate state for BINARY benchmarks so the {@code stringLength} 
parameter only
+   * creates trials for binary-related benchmark methods, not for fixed-width 
types.
+   */
+  @State(Scope.Thread)
+  public static class BinaryState {
+    /** Short (10), medium (100), and long (1000) strings. */
+    @Param({"10", "100", "1000"})
+    public int stringLength;
+
+    Binary[] data;
+
+    @Setup(Level.Trial)
+    public void setup() {
+      data = TestDataFactory.generateBinaryData(VALUE_COUNT, stringLength, 0, 
TestDataFactory.DEFAULT_SEED);
+    }
+  }
+
+  // ---- FLBA state: parameterized by fixed byte length ----
+
+  /**
+   * Separate state for FIXED_LEN_BYTE_ARRAY benchmarks so the {@code 
fixedLength}
+   * parameter only creates trials for FLBA-related benchmark methods.
+   * Values: 2 = FLOAT16, 12 = INT96, 16 = UUID.
+   */
+  @State(Scope.Thread)
+  public static class FlbaState {
+    /** FLOAT16 (2), INT96 (12), UUID (16). */
+    @Param({"2", "12", "16"})
+    public int fixedLength;
+
+    Binary[] data;
+
+    @Setup(Level.Trial)
+    public void setup() {
+      data = TestDataFactory.generateFixedLenByteArrays(
+          VALUE_COUNT, fixedLength, 0, TestDataFactory.DEFAULT_SEED);
+    }
+  }
+
+  // ---- Writer factories ----
+
+  private static PlainValuesWriter newWriter() {
+    return new PlainValuesWriter(INIT_SLAB_SIZE, PAGE_SIZE, new 
HeapByteBufferAllocator());
+  }
+
+  private static BooleanPlainValuesWriter newBoolWriter() {
+    return new BooleanPlainValuesWriter();
+  }
+
+  private static FixedLenByteArrayPlainValuesWriter newFlbaWriter(int 
fixedLength) {
+    return new FixedLenByteArrayPlainValuesWriter(
+        fixedLength, INIT_SLAB_SIZE, PAGE_SIZE, new HeapByteBufferAllocator());
+  }
+
+  // ---- BOOLEAN ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeBoolean() throws IOException {
+    ValuesWriter w = newBoolWriter();
+    for (boolean v : boolData) {
+      w.writeBoolean(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- INT32 ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeInt() throws IOException {
+    PlainValuesWriter w = newWriter();
+    for (int v : intData) {
+      w.writeInteger(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- INT64 ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeLong() throws IOException {
+    PlainValuesWriter w = newWriter();
+    for (long v : longData) {
+      w.writeLong(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- FLOAT ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeFloat() throws IOException {
+    PlainValuesWriter w = newWriter();
+    for (float v : floatData) {
+      w.writeFloat(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- DOUBLE ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeDouble() throws IOException {
+    PlainValuesWriter w = newWriter();
+    for (double v : doubleData) {
+      w.writeDouble(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- BINARY (parameterized by string length) ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeBinary(BinaryState state) throws IOException {
+    PlainValuesWriter w = newWriter();
+    for (Binary v : state.data) {
+      w.writeBytes(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+
+  // ---- FIXED_LEN_BYTE_ARRAY (parameterized by fixed length) ----
+
+  @Benchmark
+  @OperationsPerInvocation(VALUE_COUNT)
+  public byte[] encodeFixedLenByteArray(FlbaState state) throws IOException {
+    FixedLenByteArrayPlainValuesWriter w = newFlbaWriter(state.fixedLength);
+    for (Binary v : state.data) {
+      w.writeBytes(v);
+    }
+    byte[] bytes = w.getBytes().toByteArray();
+    w.close();
+    return bytes;
+  }
+}
diff --git 
a/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/TestDataFactory.java
 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/TestDataFactory.java
new file mode 100644
index 000000000..e666ebe6f
--- /dev/null
+++ 
b/parquet-benchmarks/src/main/java/org/apache/parquet/benchmarks/TestDataFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.benchmarks;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+import org.apache.parquet.io.api.Binary;
+
+/**
+ * Utility class for generating test data for encoding benchmarks.
+ */
+public final class TestDataFactory {
+
+  /** Default RNG seed used across benchmarks for deterministic data. */
+  public static final long DEFAULT_SEED = 42L;
+
+  private TestDataFactory() {}
+
+  // ---- Fixed-length byte array data generation ----
+
+  /**
+   * Generates fixed-length byte arrays with the specified cardinality.
+   *
+   * @param count      number of values
+   * @param length     byte length of each value
+   * @param distinct   number of distinct values (0 means all unique)
+   * @param seed       RNG seed
+   */
+  public static Binary[] generateFixedLenByteArrays(int count, int length, int 
distinct, long seed) {
+    Random random = new Random(seed);
+    if (distinct > 0) {
+      Binary[] palette = new Binary[distinct];
+      for (int i = 0; i < distinct; i++) {
+        byte[] bytes = new byte[length];
+        random.nextBytes(bytes);
+        palette[i] = Binary.fromConstantByteArray(bytes);
+      }
+      Binary[] data = new Binary[count];
+      for (int i = 0; i < count; i++) {
+        data[i] = palette[random.nextInt(distinct)];
+      }
+      return data;
+    } else {
+      Binary[] data = new Binary[count];
+      for (int i = 0; i < count; i++) {
+        byte[] bytes = new byte[length];
+        random.nextBytes(bytes);
+        data[i] = Binary.fromConstantByteArray(bytes);
+      }
+      return data;
+    }
+  }
+
+  // ---- Binary data generation ----
+
+  /**
+   * Generates binary strings of the given length with the specified 
cardinality.
+   *
+   * @param count         number of values
+   * @param stringLength  length of each string
+   * @param distinct      number of distinct values (0 means all unique)
+   * @param seed          RNG seed
+   */
+  public static Binary[] generateBinaryData(int count, int stringLength, int 
distinct, long seed) {
+    Random random = new Random(seed);
+    Binary[] data = new Binary[count];
+    if (distinct > 0) {
+      Binary[] dictionary = new Binary[distinct];
+      for (int i = 0; i < distinct; i++) {
+        dictionary[i] = Binary.fromConstantByteArray(
+            randomString(stringLength, 
random).getBytes(StandardCharsets.UTF_8));
+      }
+      for (int i = 0; i < count; i++) {
+        data[i] = dictionary[random.nextInt(distinct)];
+      }
+    } else {
+      for (int i = 0; i < count; i++) {
+        data[i] = Binary.fromConstantByteArray(
+            randomString(stringLength, 
random).getBytes(StandardCharsets.UTF_8));
+      }
+    }
+    return data;
+  }
+
+  private static String randomString(int length, Random random) {
+    StringBuilder sb = new StringBuilder(length);
+    for (int i = 0; i < length; i++) {
+      sb.append((char) ('a' + random.nextInt(26)));
+    }
+    return sb.toString();
+  }
+}
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BinaryPlainValuesReader.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BinaryPlainValuesReader.java
index 6ce2f31a4..34f49b970 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BinaryPlainValuesReader.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BinaryPlainValuesReader.java
@@ -19,44 +19,56 @@
 package org.apache.parquet.column.values.plain;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import org.apache.parquet.bytes.ByteBufferInputStream;
-import org.apache.parquet.bytes.BytesUtils;
 import org.apache.parquet.column.values.ValuesReader;
 import org.apache.parquet.io.ParquetDecodingException;
 import org.apache.parquet.io.api.Binary;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
+/**
+ * Plain encoding reader for BINARY values.
+ *
+ * <p>Reads directly from a {@link ByteBuffer} with {@link 
ByteOrder#LITTLE_ENDIAN} byte order,
+ * using {@link ByteBuffer#getInt()} for the 4-byte length prefix instead of 4 
individual
+ * {@code InputStream.read()} calls through {@link 
org.apache.parquet.bytes.BytesUtils#readIntLittleEndian}.
+ */
 public class BinaryPlainValuesReader extends ValuesReader {
-  private static final Logger LOG = 
LoggerFactory.getLogger(BinaryPlainValuesReader.class);
-  private ByteBufferInputStream in;
+  private static final ByteBuffer EMPTY_LE_BUFFER =
+      ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();
+
+  private ByteBuffer buffer;
 
   @Override
   public Binary readBytes() {
     try {
-      int length = BytesUtils.readIntLittleEndian(in);
-      return Binary.fromConstantByteBuffer(in.slice(length));
-    } catch (IOException | RuntimeException e) {
-      throw new ParquetDecodingException("could not read bytes at offset " + 
in.position(), e);
+      int length = buffer.getInt();
+      ByteBuffer valueSlice = buffer.slice();
+      valueSlice.limit(length);
+      buffer.position(buffer.position() + length);
+      return Binary.fromConstantByteBuffer(valueSlice);
+    } catch (RuntimeException e) {
+      throw new ParquetDecodingException("could not read bytes at offset " + 
buffer.position(), e);
     }
   }
 
   @Override
   public void skip() {
     try {
-      int length = BytesUtils.readIntLittleEndian(in);
-      in.skipFully(length);
-    } catch (IOException | RuntimeException e) {
-      throw new ParquetDecodingException("could not skip bytes at offset " + 
in.position(), e);
+      int length = buffer.getInt();
+      buffer.position(buffer.position() + length);
+    } catch (RuntimeException e) {
+      throw new ParquetDecodingException("could not skip bytes at offset " + 
buffer.position(), e);
     }
   }
 
   @Override
   public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
-    LOG.debug(
-        "init from page at offset {} for length {}",
-        stream.position(),
-        (stream.available() - stream.position()));
-    this.in = stream.remainingStream();
+    int available = stream.available();
+    if (available > 0) {
+      this.buffer = stream.slice(available).order(ByteOrder.LITTLE_ENDIAN);
+    } else {
+      this.buffer = EMPTY_LE_BUFFER.duplicate();
+    }
   }
 }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesReader.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesReader.java
index 22ca2d567..a221a957b 100755
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesReader.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesReader.java
@@ -18,57 +18,72 @@
  */
 package org.apache.parquet.column.values.plain;
 
-import static org.apache.parquet.column.values.bitpacking.Packer.LITTLE_ENDIAN;
-
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.BytesUtils;
 import org.apache.parquet.column.values.ValuesReader;
-import org.apache.parquet.column.values.bitpacking.ByteBitPackingValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * encodes boolean for the plain encoding: one bit at a time (0 = false)
+ * Decodes PLAIN-encoded booleans: one bit per value, packed 8 per byte, 
little-endian
+ * bit order (bit 0 of each byte is the first value).
+ *
+ * <p>Direct bit extraction from the page ByteBuffer avoids the overhead of 
the generic
+ * bit-packing machinery ({@code ByteBitPackingValuesReader}) and intermediate
+ * {@code int[8]} buffers.
  */
 public class BooleanPlainValuesReader extends ValuesReader {
   private static final Logger LOG = 
LoggerFactory.getLogger(BooleanPlainValuesReader.class);
 
-  private ByteBitPackingValuesReader in = new ByteBitPackingValuesReader(1, 
LITTLE_ENDIAN);
+  private byte[] pageData;
+  private int pageOffset;
+  private int bitIndex;
+  private int bitCount;
 
-  /**
-   * {@inheritDoc}
-   *
-   * @see org.apache.parquet.column.values.ValuesReader#readBoolean()
-   */
   @Override
-  public boolean readBoolean() {
-    return in.readInteger() == 0 ? false : true;
+  public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
+    LOG.debug("init from page at offset {} for length {}", stream.position(), 
stream.available());
+    int effectiveBitLength = valueCount; // bitWidth = 1
+    int length = BytesUtils.paddedByteCountFromBits(effectiveBitLength);
+    length = Math.min(length, stream.available());
+    ByteBuffer buf = stream.slice(length);
+
+    // Bulk access: use backing array directly if available, otherwise copy 
once.
+    if (buf.hasArray()) {
+      pageData = buf.array();
+      pageOffset = buf.arrayOffset() + buf.position();
+    } else {
+      pageData = new byte[length];
+      buf.get(pageData);
+      pageOffset = 0;
+    }
+    bitIndex = 0;
+    bitCount = length * 8;
+    updateNextOffset(length);
   }
 
-  /**
-   * {@inheritDoc}
-   *
-   * @see org.apache.parquet.column.values.ValuesReader#skip()
-   */
   @Override
-  public void skip() {
-    in.readInteger();
+  public boolean readBoolean() {
+    if (bitIndex >= bitCount) {
+      throw new ParquetDecodingException(
+          "attempt to read beyond end of boolean page: bitIndex=" + bitIndex + 
" bitCount=" + bitCount);
+    }
+    int byteIdx = pageOffset + (bitIndex >>> 3);
+    int bitPos = bitIndex & 7;
+    bitIndex++;
+    return ((pageData[byteIdx] >>> bitPos) & 1) != 0;
   }
 
-  /**
-   * {@inheritDoc}
-   *
-   * @see org.apache.parquet.column.values.ValuesReader#initFromPage(int, 
ByteBufferInputStream)
-   */
   @Override
-  public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
-    LOG.debug("init from page at offset {} for length {}", stream.position(), 
stream.available());
-    this.in.initFromPage(valueCount, stream);
+  public void skip() {
+    bitIndex++;
   }
 
-  @Deprecated
   @Override
-  public int getNextOffset() {
-    return in.getNextOffset();
+  public void skip(int n) {
+    bitIndex += n;
   }
 }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesWriter.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesWriter.java
index 7f80ec150..5fb899067 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesWriter.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/BooleanPlainValuesWriter.java
@@ -19,52 +19,74 @@
 package org.apache.parquet.column.values.plain;
 
 import static org.apache.parquet.column.Encoding.PLAIN;
-import static org.apache.parquet.column.values.bitpacking.Packer.LITTLE_ENDIAN;
 
 import org.apache.parquet.bytes.BytesInput;
+import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
 import org.apache.parquet.column.Encoding;
 import org.apache.parquet.column.values.ValuesWriter;
-import org.apache.parquet.column.values.bitpacking.ByteBitPackingValuesWriter;
 
 /**
- * An implementation of the PLAIN encoding
+ * An implementation of the PLAIN encoding for BOOLEAN values.
+ *
+ * <p>Packs booleans directly into bytes (8 per byte, LSB first) without
+ * going through the generic int-based bit-packing encoder.
  */
 public class BooleanPlainValuesWriter extends ValuesWriter {
 
-  private ByteBitPackingValuesWriter bitPackingWriter;
+  private static final int INITIAL_SLAB_SIZE = 1024;
+  private static final int MAX_CAPACITY = 64 * 1024;
+
+  private final CapacityByteArrayOutputStream baos;
+  private int currentByte;
+  private int bitsWritten;
 
   public BooleanPlainValuesWriter() {
-    bitPackingWriter = new ByteBitPackingValuesWriter(1, LITTLE_ENDIAN);
+    this.baos = new CapacityByteArrayOutputStream(INITIAL_SLAB_SIZE, 
MAX_CAPACITY);
+    this.currentByte = 0;
+    this.bitsWritten = 0;
   }
 
   @Override
   public final void writeBoolean(boolean v) {
-    bitPackingWriter.writeInteger(v ? 1 : 0);
+    currentByte |= ((v ? 1 : 0) << bitsWritten);
+    bitsWritten++;
+    if (bitsWritten == 8) {
+      baos.write(currentByte);
+      currentByte = 0;
+      bitsWritten = 0;
+    }
   }
 
   @Override
   public long getBufferedSize() {
-    return bitPackingWriter.getBufferedSize();
+    return baos.size() + (bitsWritten > 0 ? 1 : 0);
   }
 
   @Override
   public BytesInput getBytes() {
-    return bitPackingWriter.getBytes();
+    if (bitsWritten > 0) {
+      baos.write(currentByte);
+      currentByte = 0;
+      bitsWritten = 0;
+    }
+    return BytesInput.from(baos);
   }
 
   @Override
   public void reset() {
-    bitPackingWriter.reset();
+    baos.reset();
+    currentByte = 0;
+    bitsWritten = 0;
   }
 
   @Override
   public void close() {
-    bitPackingWriter.close();
+    baos.close();
   }
 
   @Override
   public long getAllocatedSize() {
-    return bitPackingWriter.getAllocatedSize();
+    return baos.getCapacity();
   }
 
   @Override
@@ -74,6 +96,6 @@ public class BooleanPlainValuesWriter extends ValuesWriter {
 
   @Override
   public String memUsageString(String prefix) {
-    return bitPackingWriter.memUsageString(prefix);
+    return String.format("%s BooleanPlainValuesWriter %d bytes", prefix, 
getAllocatedSize());
   }
 }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesReader.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesReader.java
index adfc48892..ee54443be 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesReader.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesReader.java
@@ -19,6 +19,7 @@
 package org.apache.parquet.column.values.plain;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import org.apache.parquet.bytes.ByteBufferInputStream;
 import org.apache.parquet.column.values.ValuesReader;
 import org.apache.parquet.io.ParquetDecodingException;
@@ -28,12 +29,17 @@ import org.slf4j.LoggerFactory;
 
 /**
  * ValuesReader for FIXED_LEN_BYTE_ARRAY.
+ *
+ * <p>Reads directly from a {@link ByteBuffer}, bypassing the {@link 
ByteBufferInputStream}
+ * wrapper to avoid per-value stream overhead (remaining checks, IOException 
wrapping,
+ * virtual dispatch). The underlying page data is obtained as a single 
contiguous
+ * {@link ByteBuffer} via {@link ByteBufferInputStream#slice(int)}.
  */
 public class FixedLenByteArrayPlainValuesReader extends ValuesReader {
   private static final Logger LOG = 
LoggerFactory.getLogger(FixedLenByteArrayPlainValuesReader.class);
 
   private final int length;
-  private ByteBufferInputStream in;
+  private ByteBuffer buffer;
 
   public FixedLenByteArrayPlainValuesReader(int length) {
     this.length = length;
@@ -42,29 +48,40 @@ public class FixedLenByteArrayPlainValuesReader extends 
ValuesReader {
   @Override
   public Binary readBytes() {
     try {
-      return Binary.fromConstantByteBuffer(in.slice(length));
-    } catch (IOException | RuntimeException e) {
-      throw new ParquetDecodingException("could not read bytes at offset " + 
in.position(), e);
+      Binary result = Binary.fromConstantByteBuffer(buffer, buffer.position(), 
length);
+      buffer.position(buffer.position() + length);
+      return result;
+    } catch (RuntimeException e) {
+      throw new ParquetDecodingException("could not read bytes at offset " + 
buffer.position(), e);
     }
   }
 
   @Override
   public void skip() {
-    skip(1);
+    try {
+      buffer.position(buffer.position() + length);
+    } catch (RuntimeException e) {
+      throw new ParquetDecodingException("could not skip bytes at offset " + 
buffer.position(), e);
+    }
   }
 
   @Override
   public void skip(int n) {
     try {
-      in.skipFully(n * length);
-    } catch (IOException | RuntimeException e) {
-      throw new ParquetDecodingException("could not skip bytes at offset " + 
in.position(), e);
+      buffer.position(buffer.position() + Math.multiplyExact(n, length));
+    } catch (RuntimeException e) {
+      throw new ParquetDecodingException("could not skip bytes at offset " + 
buffer.position(), e);
     }
   }
 
   @Override
   public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
     LOG.debug("init from page at offset {} for length {}", stream.position(), 
stream.available());
-    this.in = stream.remainingStream();
+    int available = stream.available();
+    if (available > 0) {
+      this.buffer = stream.slice(available);
+    } else {
+      this.buffer = ByteBuffer.allocate(0);
+    }
   }
 }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesWriter.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesWriter.java
index dec4d1be1..b57fdef84 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesWriter.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/FixedLenByteArrayPlainValuesWriter.java
@@ -22,7 +22,6 @@ import java.io.IOException;
 import org.apache.parquet.bytes.ByteBufferAllocator;
 import org.apache.parquet.bytes.BytesInput;
 import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
-import org.apache.parquet.bytes.LittleEndianDataOutputStream;
 import org.apache.parquet.column.Encoding;
 import org.apache.parquet.column.values.ValuesWriter;
 import org.apache.parquet.io.ParquetEncodingException;
@@ -34,19 +33,15 @@ import org.slf4j.LoggerFactory;
  * ValuesWriter for FIXED_LEN_BYTE_ARRAY.
  */
 public class FixedLenByteArrayPlainValuesWriter extends ValuesWriter {
-  private static final Logger LOG = 
LoggerFactory.getLogger(PlainValuesWriter.class);
+  private static final Logger LOG = 
LoggerFactory.getLogger(FixedLenByteArrayPlainValuesWriter.class);
 
   private CapacityByteArrayOutputStream arrayOut;
-  private LittleEndianDataOutputStream out;
   private int length;
-  private ByteBufferAllocator allocator;
 
   public FixedLenByteArrayPlainValuesWriter(
       int length, int initialSize, int pageSize, ByteBufferAllocator 
allocator) {
     this.length = length;
-    this.allocator = allocator;
-    this.arrayOut = new CapacityByteArrayOutputStream(initialSize, pageSize, 
this.allocator);
-    this.out = new LittleEndianDataOutputStream(arrayOut);
+    this.arrayOut = new CapacityByteArrayOutputStream(initialSize, pageSize, 
allocator);
   }
 
   @Override
@@ -56,7 +51,7 @@ public class FixedLenByteArrayPlainValuesWriter extends 
ValuesWriter {
           "Fixed Binary size " + v.length() + " does not match field type 
length " + length);
     }
     try {
-      v.writeTo(out);
+      v.writeTo(arrayOut);
     } catch (IOException e) {
       throw new ParquetEncodingException("could not write fixed bytes", e);
     }
@@ -69,11 +64,6 @@ public class FixedLenByteArrayPlainValuesWriter extends 
ValuesWriter {
 
   @Override
   public BytesInput getBytes() {
-    try {
-      out.flush();
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write page", e);
-    }
     LOG.debug("writing a buffer of size {}", arrayOut.size());
     return BytesInput.from(arrayOut);
   }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesReader.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesReader.java
index a0c7af739..7a4d18eb7 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesReader.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesReader.java
@@ -19,25 +19,39 @@
 package org.apache.parquet.column.values.plain;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import org.apache.parquet.bytes.ByteBufferInputStream;
-import org.apache.parquet.bytes.LittleEndianDataInputStream;
 import org.apache.parquet.column.values.ValuesReader;
 import org.apache.parquet.io.ParquetDecodingException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Plain encoding for float, double, int, long
+ * Plain encoding for float, double, int, long.
+ *
+ * <p>Reads directly from a {@link ByteBuffer} with {@link 
ByteOrder#LITTLE_ENDIAN} byte order,
+ * bypassing the deprecated {@code LittleEndianDataInputStream} wrapper to 
avoid per-value virtual
+ * dispatch overhead. The underlying page data is obtained as a single 
contiguous {@link ByteBuffer}
+ * via {@link ByteBufferInputStream#slice(int)}.
  */
 public abstract class PlainValuesReader extends ValuesReader {
   private static final Logger LOG = 
LoggerFactory.getLogger(PlainValuesReader.class);
 
-  protected LittleEndianDataInputStream in;
+  private static final ByteBuffer EMPTY_LE_BUFFER =
+      ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();
+
+  protected ByteBuffer buffer;
 
   @Override
   public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
     LOG.debug("init from page at offset {} for length {}", stream.position(), 
stream.available());
-    this.in = new LittleEndianDataInputStream(stream.remainingStream());
+    int available = stream.available();
+    if (available > 0) {
+      this.buffer = stream.slice(available).order(ByteOrder.LITTLE_ENDIAN);
+    } else {
+      this.buffer = EMPTY_LE_BUFFER.duplicate();
+    }
   }
 
   @Override
@@ -45,20 +59,13 @@ public abstract class PlainValuesReader extends 
ValuesReader {
     skip(1);
   }
 
-  void skipBytesFully(int n) throws IOException {
-    int skipped = 0;
-    while (skipped < n) {
-      skipped += in.skipBytes(n - skipped);
-    }
-  }
-
   public static class DoublePlainValuesReader extends PlainValuesReader {
 
     @Override
     public void skip(int n) {
       try {
-        skipBytesFully(n * 8);
-      } catch (IOException e) {
+        buffer.position(buffer.position() + Math.multiplyExact(n, 8));
+      } catch (RuntimeException e) {
         throw new ParquetDecodingException("could not skip " + n + " double 
values", e);
       }
     }
@@ -66,8 +73,8 @@ public abstract class PlainValuesReader extends ValuesReader {
     @Override
     public double readDouble() {
       try {
-        return in.readDouble();
-      } catch (IOException e) {
+        return buffer.getDouble();
+      } catch (RuntimeException e) {
         throw new ParquetDecodingException("could not read double", e);
       }
     }
@@ -78,17 +85,17 @@ public abstract class PlainValuesReader extends 
ValuesReader {
     @Override
     public void skip(int n) {
       try {
-        skipBytesFully(n * 4);
-      } catch (IOException e) {
-        throw new ParquetDecodingException("could not skip " + n + " floats", 
e);
+        buffer.position(buffer.position() + Math.multiplyExact(n, 4));
+      } catch (RuntimeException e) {
+        throw new ParquetDecodingException("could not skip " + n + " float 
values", e);
       }
     }
 
     @Override
     public float readFloat() {
       try {
-        return in.readFloat();
-      } catch (IOException e) {
+        return buffer.getFloat();
+      } catch (RuntimeException e) {
         throw new ParquetDecodingException("could not read float", e);
       }
     }
@@ -99,17 +106,17 @@ public abstract class PlainValuesReader extends 
ValuesReader {
     @Override
     public void skip(int n) {
       try {
-        in.skipBytes(n * 4);
-      } catch (IOException e) {
-        throw new ParquetDecodingException("could not skip " + n + " ints", e);
+        buffer.position(buffer.position() + Math.multiplyExact(n, 4));
+      } catch (RuntimeException e) {
+        throw new ParquetDecodingException("could not skip " + n + " int 
values", e);
       }
     }
 
     @Override
     public int readInteger() {
       try {
-        return in.readInt();
-      } catch (IOException e) {
+        return buffer.getInt();
+      } catch (RuntimeException e) {
         throw new ParquetDecodingException("could not read int", e);
       }
     }
@@ -120,17 +127,17 @@ public abstract class PlainValuesReader extends 
ValuesReader {
     @Override
     public void skip(int n) {
       try {
-        in.skipBytes(n * 8);
-      } catch (IOException e) {
-        throw new ParquetDecodingException("could not skip " + n + " longs", 
e);
+        buffer.position(buffer.position() + Math.multiplyExact(n, 8));
+      } catch (RuntimeException e) {
+        throw new ParquetDecodingException("could not skip " + n + " long 
values", e);
       }
     }
 
     @Override
     public long readLong() {
       try {
-        return in.readLong();
-      } catch (IOException e) {
+        return buffer.getLong();
+      } catch (RuntimeException e) {
         throw new ParquetDecodingException("could not read long", e);
       }
     }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesWriter.java
 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesWriter.java
index c7069bc09..be5dfc0fa 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesWriter.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/column/values/plain/PlainValuesWriter.java
@@ -23,7 +23,6 @@ import java.nio.charset.Charset;
 import org.apache.parquet.bytes.ByteBufferAllocator;
 import org.apache.parquet.bytes.BytesInput;
 import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
-import org.apache.parquet.bytes.LittleEndianDataOutputStream;
 import org.apache.parquet.column.Encoding;
 import org.apache.parquet.column.values.ValuesWriter;
 import org.apache.parquet.io.ParquetEncodingException;
@@ -41,18 +40,16 @@ public class PlainValuesWriter extends ValuesWriter {
   public static final Charset CHARSET = Charset.forName("UTF-8");
 
   private CapacityByteArrayOutputStream arrayOut;
-  private LittleEndianDataOutputStream out;
 
   public PlainValuesWriter(int initialSize, int pageSize, ByteBufferAllocator 
allocator) {
     arrayOut = new CapacityByteArrayOutputStream(initialSize, pageSize, 
allocator);
-    out = new LittleEndianDataOutputStream(arrayOut);
   }
 
   @Override
   public final void writeBytes(Binary v) {
     try {
-      out.writeInt(v.length());
-      v.writeTo(out);
+      arrayOut.writeInt(v.length());
+      v.writeTo(arrayOut);
     } catch (IOException e) {
       throw new ParquetEncodingException("could not write bytes", e);
     }
@@ -60,47 +57,27 @@ public class PlainValuesWriter extends ValuesWriter {
 
   @Override
   public final void writeInteger(int v) {
-    try {
-      out.writeInt(v);
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write int", e);
-    }
+    arrayOut.writeInt(v);
   }
 
   @Override
   public final void writeLong(long v) {
-    try {
-      out.writeLong(v);
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write long", e);
-    }
+    arrayOut.writeLong(v);
   }
 
   @Override
   public final void writeFloat(float v) {
-    try {
-      out.writeFloat(v);
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write float", e);
-    }
+    arrayOut.writeInt(Float.floatToIntBits(v));
   }
 
   @Override
   public final void writeDouble(double v) {
-    try {
-      out.writeDouble(v);
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write double", e);
-    }
+    arrayOut.writeLong(Double.doubleToLongBits(v));
   }
 
   @Override
   public void writeByte(int value) {
-    try {
-      out.write(value);
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write byte", e);
-    }
+    arrayOut.write(value);
   }
 
   @Override
@@ -110,11 +87,6 @@ public class PlainValuesWriter extends ValuesWriter {
 
   @Override
   public BytesInput getBytes() {
-    try {
-      out.flush();
-    } catch (IOException e) {
-      throw new ParquetEncodingException("could not write page", e);
-    }
     if (LOG.isDebugEnabled()) LOG.debug("writing a buffer of size {}", 
arrayOut.size());
     return BytesInput.from(arrayOut);
   }
@@ -127,7 +99,6 @@ public class PlainValuesWriter extends ValuesWriter {
   @Override
   public void close() {
     arrayOut.close();
-    out.close();
   }
 
   @Override
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBinaryPlainValuesWriterReader.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBinaryPlainValuesWriterReader.java
new file mode 100644
index 000000000..da29bddb9
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBinaryPlainValuesWriterReader.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.plain;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.bytes.TrackingByteBufferAllocator;
+import org.apache.parquet.io.api.Binary;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link BinaryPlainValuesReader} (variable-length BINARY)
+ * covering scalar round-trips via {@link PlainValuesWriter}.
+ */
+public class TestBinaryPlainValuesWriterReader {
+
+  private TrackingByteBufferAllocator allocator;
+
+  @Before
+  public void initAllocator() {
+    allocator = TrackingByteBufferAllocator.wrap(new 
HeapByteBufferAllocator());
+  }
+
+  @After
+  public void closeAllocator() {
+    allocator.close();
+  }
+
+  private PlainValuesWriter newWriter() {
+    return new PlainValuesWriter(1024, 64 * 1024, allocator);
+  }
+
+  private ByteBufferInputStream wrapForReading(PlainValuesWriter writer) 
throws IOException {
+    byte[] bytes = writer.getBytes().toByteArray();
+    return ByteBufferInputStream.wrap(ByteBuffer.wrap(bytes));
+  }
+
+  private Binary binaryFromString(String s) {
+    return 
Binary.fromConstantByteArray(s.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+  }
+
+  // ---- Scalar round-trip ----
+
+  @Test
+  public void testScalarRoundTrip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      String[] strings = {"hello", "", "world", "a", "longer string value"};
+      for (String s : strings) {
+        writer.writeBytes(binaryFromString(s));
+      }
+
+      BinaryPlainValuesReader reader = new BinaryPlainValuesReader();
+      reader.initFromPage(strings.length, wrapForReading(writer));
+
+      for (String s : strings) {
+        assertEquals(s, reader.readBytes().toStringUsingUTF8());
+      }
+    }
+  }
+
+  // ---- Skip ----
+
+  @Test
+  public void testSkip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeBytes(binaryFromString("first"));
+      writer.writeBytes(binaryFromString("second"));
+      writer.writeBytes(binaryFromString("third"));
+
+      BinaryPlainValuesReader reader = new BinaryPlainValuesReader();
+      reader.initFromPage(3, wrapForReading(writer));
+
+      reader.skip(); // skip "first"
+      assertEquals("second", reader.readBytes().toStringUsingUTF8());
+    }
+  }
+
+  // ---- Empty page ----
+
+  @Test
+  public void testEmptyPage() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      BinaryPlainValuesReader reader = new BinaryPlainValuesReader();
+      reader.initFromPage(0, wrapForReading(writer));
+      // Should not throw
+    }
+  }
+
+  // ---- Binary content (non-UTF8) ----
+
+  @Test
+  public void testBinaryContent() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      byte[] raw1 = {0, 1, 2, (byte) 0xFF, (byte) 0xFE};
+      byte[] raw2 = {(byte) 0x80, 0};
+      Binary bin1 = Binary.fromConstantByteArray(raw1);
+      Binary bin2 = Binary.fromConstantByteArray(raw2);
+
+      writer.writeBytes(bin1);
+      writer.writeBytes(bin2);
+
+      BinaryPlainValuesReader reader = new BinaryPlainValuesReader();
+      reader.initFromPage(2, wrapForReading(writer));
+
+      assertArrayEquals(raw1, reader.readBytes().getBytes());
+      assertArrayEquals(raw2, reader.readBytes().getBytes());
+    }
+  }
+}
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBooleanPlainValuesWriterReader.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBooleanPlainValuesWriterReader.java
new file mode 100644
index 000000000..aba92ec68
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestBooleanPlainValuesWriterReader.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.plain;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.Encoding;
+import org.junit.Test;
+
+/**
+ * Tests for {@link BooleanPlainValuesWriter} and {@link 
BooleanPlainValuesReader}
+ * covering scalar round-trips, edge cases with partial bytes, and skip.
+ */
+public class TestBooleanPlainValuesWriterReader {
+
+  private ByteBufferInputStream wrapForReading(BooleanPlainValuesWriter 
writer) throws IOException {
+    byte[] bytes = writer.getBytes().toByteArray();
+    return ByteBufferInputStream.wrap(ByteBuffer.wrap(bytes));
+  }
+
+  // ---- Encoding metadata ----
+
+  @Test
+  public void testEncoding() {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      assertEquals(Encoding.PLAIN, writer.getEncoding());
+    }
+  }
+
+  // ---- Scalar round-trip ----
+
+  @Test
+  public void testScalarRoundTrip() throws IOException {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      boolean[] expected = {true, false, true, true, false, false, true, 
false};
+      for (boolean v : expected) {
+        writer.writeBoolean(v);
+      }
+
+      BooleanPlainValuesReader reader = new BooleanPlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals("value at index " + i, expected[i], reader.readBoolean());
+      }
+    }
+  }
+
+  // ---- Partial byte (< 8 booleans) ----
+
+  @Test
+  public void testPartialByte() throws IOException {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      boolean[] expected = {true, false, true};
+      for (boolean v : expected) {
+        writer.writeBoolean(v);
+      }
+
+      BooleanPlainValuesReader reader = new BooleanPlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals("value at index " + i, expected[i], reader.readBoolean());
+      }
+    }
+  }
+
+  // ---- Skip ----
+
+  @Test
+  public void testSkip() throws IOException {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      boolean[] values = {true, false, true, false, true};
+      for (boolean v : values) {
+        writer.writeBoolean(v);
+      }
+
+      BooleanPlainValuesReader reader = new BooleanPlainValuesReader();
+      reader.initFromPage(values.length, wrapForReading(writer));
+
+      reader.skip(); // skip true
+      assertEquals(false, reader.readBoolean());
+      reader.skip(2); // skip true, false
+      assertEquals(true, reader.readBoolean());
+    }
+  }
+
+  // ---- Single value ----
+
+  @Test
+  public void testSingleValue() throws IOException {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      writer.writeBoolean(true);
+
+      BooleanPlainValuesReader reader = new BooleanPlainValuesReader();
+      reader.initFromPage(1, wrapForReading(writer));
+
+      assertEquals(true, reader.readBoolean());
+    }
+  }
+
+  // ---- Reset ----
+
+  @Test
+  public void testWriterReset() throws IOException {
+    try (BooleanPlainValuesWriter writer = new BooleanPlainValuesWriter()) {
+      writer.writeBoolean(true);
+      writer.writeBoolean(false);
+      writer.reset();
+      assertEquals(0, writer.getBufferedSize());
+
+      writer.writeBoolean(false);
+      writer.writeBoolean(true);
+
+      BooleanPlainValuesReader reader = new BooleanPlainValuesReader();
+      reader.initFromPage(2, wrapForReading(writer));
+
+      assertEquals(false, reader.readBoolean());
+      assertEquals(true, reader.readBoolean());
+    }
+  }
+}
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
new file mode 100644
index 000000000..09ec6dbe7
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.plain;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.bytes.TrackingByteBufferAllocator;
+import org.apache.parquet.column.Encoding;
+import org.apache.parquet.io.api.Binary;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link FixedLenByteArrayPlainValuesWriter} and
+ * {@link FixedLenByteArrayPlainValuesReader} covering scalar
+ * round-trips for fixed-length byte arrays.
+ */
+public class TestFixedLenByteArrayPlainValuesWriterReader {
+
+  private static final int FIXED_LEN = 12;
+
+  private TrackingByteBufferAllocator allocator;
+
+  @Before
+  public void initAllocator() {
+    allocator = TrackingByteBufferAllocator.wrap(new 
HeapByteBufferAllocator());
+  }
+
+  @After
+  public void closeAllocator() {
+    allocator.close();
+  }
+
+  private FixedLenByteArrayPlainValuesWriter newWriter() {
+    return new FixedLenByteArrayPlainValuesWriter(FIXED_LEN, 1024, 64 * 1024, 
allocator);
+  }
+
+  private ByteBufferInputStream 
wrapForReading(FixedLenByteArrayPlainValuesWriter writer) throws IOException {
+    byte[] bytes = writer.getBytes().toByteArray();
+    return ByteBufferInputStream.wrap(ByteBuffer.wrap(bytes));
+  }
+
+  private Binary fixedBinary(int seed) {
+    byte[] data = new byte[FIXED_LEN];
+    for (int i = 0; i < FIXED_LEN; i++) {
+      data[i] = (byte) ((seed + i) & 0xFF);
+    }
+    return Binary.fromConstantByteArray(data);
+  }
+
+  // ---- Encoding metadata ----
+
+  @Test
+  public void testEncoding() {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      assertEquals(Encoding.PLAIN, writer.getEncoding());
+    }
+  }
+
+  // ---- Scalar round-trip ----
+
+  @Test
+  public void testScalarRoundTrip() throws IOException {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      Binary[] expected = {fixedBinary(0), fixedBinary(100), fixedBinary(200)};
+      for (Binary v : expected) {
+        writer.writeBytes(v);
+      }
+
+      FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertArrayEquals(
+            "value at index " + i,
+            expected[i].getBytes(),
+            reader.readBytes().getBytes());
+      }
+    }
+  }
+
+  // ---- Skip ----
+
+  @Test
+  public void testSkip() throws IOException {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      writer.writeBytes(fixedBinary(1));
+      writer.writeBytes(fixedBinary(2));
+      writer.writeBytes(fixedBinary(3));
+      writer.writeBytes(fixedBinary(4));
+
+      FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+      reader.initFromPage(4, wrapForReading(writer));
+
+      reader.skip(); // skip 1
+      assertArrayEquals(fixedBinary(2).getBytes(), 
reader.readBytes().getBytes());
+      reader.skip(1); // skip 3
+      assertArrayEquals(fixedBinary(4).getBytes(), 
reader.readBytes().getBytes());
+    }
+  }
+
+  // ---- Wrong length rejection ----
+
+  @Test
+  public void testRejectWrongLengthScalar() {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      Binary wrongLen = Binary.fromConstantByteArray(new byte[FIXED_LEN + 1]);
+      try {
+        writer.writeBytes(wrongLen);
+        fail("Should have thrown IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+        // expected
+      }
+    }
+  }
+
+  // ---- Reset ----
+
+  @Test
+  public void testWriterReset() throws IOException {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      writer.writeBytes(fixedBinary(99));
+      writer.reset();
+      assertEquals(0, writer.getBufferedSize());
+
+      writer.writeBytes(fixedBinary(42));
+
+      FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+      reader.initFromPage(1, wrapForReading(writer));
+
+      assertArrayEquals(fixedBinary(42).getBytes(), 
reader.readBytes().getBytes());
+    }
+  }
+
+  // ---- Empty page ----
+
+  @Test
+  public void testEmptyPage() throws IOException {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+      reader.initFromPage(0, wrapForReading(writer));
+      // Should not throw
+    }
+  }
+}
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestPlainValuesWriterReader.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestPlainValuesWriterReader.java
new file mode 100644
index 000000000..6b98f74b0
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestPlainValuesWriterReader.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.plain;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.bytes.HeapByteBufferAllocator;
+import org.apache.parquet.bytes.TrackingByteBufferAllocator;
+import org.apache.parquet.column.Encoding;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link PlainValuesWriter} and {@link PlainValuesReader} covering
+ * scalar read/write round-trips for int, long, float, and double.
+ */
+public class TestPlainValuesWriterReader {
+
+  private TrackingByteBufferAllocator allocator;
+
+  @Before
+  public void initAllocator() {
+    allocator = TrackingByteBufferAllocator.wrap(new 
HeapByteBufferAllocator());
+  }
+
+  @After
+  public void closeAllocator() {
+    allocator.close();
+  }
+
+  private PlainValuesWriter newWriter() {
+    return new PlainValuesWriter(1024, 64 * 1024, allocator);
+  }
+
+  private ByteBufferInputStream wrapForReading(PlainValuesWriter writer) 
throws IOException {
+    byte[] bytes = writer.getBytes().toByteArray();
+    return ByteBufferInputStream.wrap(ByteBuffer.wrap(bytes));
+  }
+
+  // ---- Encoding metadata ----
+
+  @Test
+  public void testEncoding() {
+    try (PlainValuesWriter writer = newWriter()) {
+      assertEquals(Encoding.PLAIN, writer.getEncoding());
+    }
+  }
+
+  // ---- Integer scalar ----
+
+  @Test
+  public void testIntegerScalarRoundTrip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      int[] expected = {0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE, 42, 
-42};
+      for (int v : expected) {
+        writer.writeInteger(v);
+      }
+
+      PlainValuesReader.IntegerPlainValuesReader reader = new 
PlainValuesReader.IntegerPlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals("value at index " + i, expected[i], reader.readInteger());
+      }
+    }
+  }
+
+  // ---- Long scalar ----
+
+  @Test
+  public void testLongScalarRoundTrip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      long[] expected = {0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 
123456789L};
+      for (long v : expected) {
+        writer.writeLong(v);
+      }
+
+      PlainValuesReader.LongPlainValuesReader reader = new 
PlainValuesReader.LongPlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals("value at index " + i, expected[i], reader.readLong());
+      }
+    }
+  }
+
+  // ---- Float scalar ----
+
+  @Test
+  public void testFloatScalarRoundTrip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      float[] expected = {
+        0.0f,
+        1.5f,
+        -1.5f,
+        Float.MIN_VALUE,
+        Float.MAX_VALUE,
+        Float.NaN,
+        Float.POSITIVE_INFINITY,
+        Float.NEGATIVE_INFINITY
+      };
+      for (float v : expected) {
+        writer.writeFloat(v);
+      }
+
+      PlainValuesReader.FloatPlainValuesReader reader = new 
PlainValuesReader.FloatPlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals(
+            "value at index " + i,
+            Float.floatToIntBits(expected[i]),
+            Float.floatToIntBits(reader.readFloat()));
+      }
+    }
+  }
+
+  // ---- Double scalar ----
+
+  @Test
+  public void testDoubleScalarRoundTrip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      double[] expected = {
+        0.0,
+        1.5,
+        -1.5,
+        Double.MIN_VALUE,
+        Double.MAX_VALUE,
+        Double.NaN,
+        Double.POSITIVE_INFINITY,
+        Double.NEGATIVE_INFINITY
+      };
+      for (double v : expected) {
+        writer.writeDouble(v);
+      }
+
+      PlainValuesReader.DoublePlainValuesReader reader = new 
PlainValuesReader.DoublePlainValuesReader();
+      reader.initFromPage(expected.length, wrapForReading(writer));
+
+      for (int i = 0; i < expected.length; i++) {
+        assertEquals(
+            "value at index " + i,
+            Double.doubleToLongBits(expected[i]),
+            Double.doubleToLongBits(reader.readDouble()));
+      }
+    }
+  }
+
+  // ---- Skip ----
+
+  @Test
+  public void testIntegerSkipThenRead() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeInteger(1);
+      writer.writeInteger(2);
+      writer.writeInteger(3);
+      writer.writeInteger(4);
+
+      PlainValuesReader.IntegerPlainValuesReader reader = new 
PlainValuesReader.IntegerPlainValuesReader();
+      reader.initFromPage(4, wrapForReading(writer));
+
+      reader.skip(); // skip 1
+      assertEquals(2, reader.readInteger());
+      reader.skip(1); // skip 3
+      assertEquals(4, reader.readInteger());
+    }
+  }
+
+  @Test
+  public void testLongSkip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeLong(100L);
+      writer.writeLong(200L);
+      writer.writeLong(300L);
+
+      PlainValuesReader.LongPlainValuesReader reader = new 
PlainValuesReader.LongPlainValuesReader();
+      reader.initFromPage(3, wrapForReading(writer));
+
+      reader.skip(2);
+      assertEquals(300L, reader.readLong());
+    }
+  }
+
+  @Test
+  public void testFloatSkip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeFloat(1.0f);
+      writer.writeFloat(2.0f);
+      writer.writeFloat(3.0f);
+
+      PlainValuesReader.FloatPlainValuesReader reader = new 
PlainValuesReader.FloatPlainValuesReader();
+      reader.initFromPage(3, wrapForReading(writer));
+
+      reader.skip();
+      assertEquals(Float.floatToIntBits(2.0f), 
Float.floatToIntBits(reader.readFloat()));
+    }
+  }
+
+  @Test
+  public void testDoubleSkip() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeDouble(1.0);
+      writer.writeDouble(2.0);
+      writer.writeDouble(3.0);
+
+      PlainValuesReader.DoublePlainValuesReader reader = new 
PlainValuesReader.DoublePlainValuesReader();
+      reader.initFromPage(3, wrapForReading(writer));
+
+      reader.skip();
+      assertEquals(Double.doubleToLongBits(2.0), 
Double.doubleToLongBits(reader.readDouble()));
+    }
+  }
+
+  // ---- Reset ----
+
+  @Test
+  public void testWriterReset() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      writer.writeInteger(999);
+      writer.reset();
+      assertEquals(0, writer.getBufferedSize());
+
+      writer.writeInteger(42);
+
+      PlainValuesReader.IntegerPlainValuesReader reader = new 
PlainValuesReader.IntegerPlainValuesReader();
+      reader.initFromPage(1, wrapForReading(writer));
+
+      assertEquals(42, reader.readInteger());
+    }
+  }
+
+  // ---- Empty page ----
+
+  @Test
+  public void testEmptyPage() throws IOException {
+    try (PlainValuesWriter writer = newWriter()) {
+      PlainValuesReader.IntegerPlainValuesReader reader = new 
PlainValuesReader.IntegerPlainValuesReader();
+      reader.initFromPage(0, wrapForReading(writer));
+      // Should not throw — no values to read
+    }
+  }
+}
diff --git 
a/parquet-common/src/main/java/org/apache/parquet/bytes/BytesInput.java 
b/parquet-common/src/main/java/org/apache/parquet/bytes/BytesInput.java
index 0e6614074..ca139d924 100644
--- a/parquet-common/src/main/java/org/apache/parquet/bytes/BytesInput.java
+++ b/parquet-common/src/main/java/org/apache/parquet/bytes/BytesInput.java
@@ -334,7 +334,12 @@ public abstract class BytesInput {
    * @return a new InputStream materializing the contents of this input
    * @throws IOException if there is an exception reading
    */
+  @SuppressWarnings("deprecation")
   public ByteBufferInputStream toInputStream() throws IOException {
+    ByteBuffer buf = getInternalByteBuffer();
+    if (buf != null) {
+      return ByteBufferInputStream.wrap(buf);
+    }
     return ByteBufferInputStream.wrap(toByteBuffer());
   }
 
@@ -608,7 +613,14 @@ public abstract class BytesInput {
 
     @Override
     void writeInto(ByteBuffer buffer) {
-      buffer.put(arrayOut.toByteArray());
+      // Use writeTo() which writes directly from the internal buf[] array,
+      // avoiding the toByteArray() copy that would allocate a full-size byte[]
+      try {
+        arrayOut.writeTo(new ByteBufferBackedOutputStream(buffer));
+      } catch (IOException e) {
+        // ByteBufferBackedOutputStream does not throw IOException
+        throw new RuntimeException("Unexpected IOException writing to 
ByteBuffer", e);
+      }
     }
 
     @Override
@@ -617,6 +629,29 @@ public abstract class BytesInput {
     }
   }
 
+  /**
+   * Thin adapter that allows writing to a {@link ByteBuffer} via the {@link 
OutputStream} interface.
+   * Used by {@link BAOSBytesInput#writeInto(ByteBuffer)} to avoid the 
intermediate copy from
+   * {@link ByteArrayOutputStream#toByteArray()}.
+   */
+  private static class ByteBufferBackedOutputStream extends OutputStream {
+    private final ByteBuffer buffer;
+
+    ByteBufferBackedOutputStream(ByteBuffer buffer) {
+      this.buffer = buffer;
+    }
+
+    @Override
+    public void write(int b) {
+      buffer.put((byte) b);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) {
+      buffer.put(b, off, len);
+    }
+  }
+
   private static class ByteArrayBytesInput extends BytesInput {
 
     private final byte[] in;
@@ -639,10 +674,37 @@ public abstract class BytesInput {
       buffer.put(in, offset, length);
     }
 
+    @Override
+    ByteBuffer getInternalByteBuffer() {
+      return java.nio.ByteBuffer.wrap(in, offset, length).slice();
+    }
+
+    @Override
+    public ByteBufferInputStream toInputStream() {
+      return ByteBufferInputStream.wrap(java.nio.ByteBuffer.wrap(in, offset, 
length));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
     public ByteBuffer toByteBuffer() throws IOException {
       return java.nio.ByteBuffer.wrap(in, offset, length);
     }
 
+    /**
+     * Zero-copy override: returns the backing array directly when fully used,
+     * skipping the base-class BAOS allocation + copy on every decompressor 
call.
+     * Returning the mutable array is safe — the base class already exposes a
+     * mutable {@code BAOS.getBuf()}.
+     */
+    @SuppressWarnings("deprecation")
+    @Override
+    public byte[] toByteArray() {
+      if (offset == 0 && length == in.length) {
+        return in;
+      }
+      return Arrays.copyOfRange(in, offset, offset + length);
+    }
+
     @Override
     public long size() {
       return length;
diff --git 
a/parquet-common/src/main/java/org/apache/parquet/bytes/CapacityByteArrayOutputStream.java
 
b/parquet-common/src/main/java/org/apache/parquet/bytes/CapacityByteArrayOutputStream.java
index d3d8b1b6d..3aa2c736a 100644
--- 
a/parquet-common/src/main/java/org/apache/parquet/bytes/CapacityByteArrayOutputStream.java
+++ 
b/parquet-common/src/main/java/org/apache/parquet/bytes/CapacityByteArrayOutputStream.java
@@ -27,6 +27,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.parquet.OutputStreamCloseException;
@@ -201,6 +202,7 @@ public class CapacityByteArrayOutputStream extends 
OutputStream {
     LOG.debug("used {} slabs, adding new slab of size {}", slabs.size(), 
nextSlabSize);
 
     this.currentSlab = allocator.allocate(nextSlabSize);
+    this.currentSlab.order(ByteOrder.LITTLE_ENDIAN);
     this.slabs.add(currentSlab);
     this.bytesAllocated = Math.addExact(this.bytesAllocated, nextSlabSize);
   }
@@ -232,6 +234,34 @@ public class CapacityByteArrayOutputStream extends 
OutputStream {
     bytesUsed = Math.addExact(bytesUsed, len);
   }
 
+  /**
+   * Writes an int in little-endian byte order directly to the underlying slab,
+   * bypassing intermediate byte array decomposition. Slabs are set to
+   * {@link ByteOrder#LITTLE_ENDIAN} order so {@code putInt} produces the 
correct encoding.
+   *
+   * @param v the int value to write
+   */
+  public void writeInt(int v) {
+    if (currentSlab.remaining() < 4) {
+      addSlab(4);
+    }
+    currentSlab.putInt(v);
+    bytesUsed = Math.addExact(bytesUsed, 4);
+  }
+
+  /**
+   * Writes a long in little-endian byte order directly to the underlying slab.
+   *
+   * @param v the long value to write
+   */
+  public void writeLong(long v) {
+    if (currentSlab.remaining() < 8) {
+      addSlab(8);
+    }
+    currentSlab.putLong(v);
+    bytesUsed = Math.addExact(bytesUsed, 8);
+  }
+
   private void writeToOutput(OutputStream out, ByteBuffer buf, int len) throws 
IOException {
     if (buf.hasArray()) {
       out.write(buf.array(), buf.arrayOffset(), len);
diff --git 
a/parquet-common/src/main/java/org/apache/parquet/bytes/LittleEndianDataOutputStream.java
 
b/parquet-common/src/main/java/org/apache/parquet/bytes/LittleEndianDataOutputStream.java
index fbc057969..9c0bb769d 100644
--- 
a/parquet-common/src/main/java/org/apache/parquet/bytes/LittleEndianDataOutputStream.java
+++ 
b/parquet-common/src/main/java/org/apache/parquet/bytes/LittleEndianDataOutputStream.java
@@ -30,6 +30,7 @@ public class LittleEndianDataOutputStream extends 
OutputStream {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(LittleEndianDataOutputStream.class);
   private final OutputStream out;
+  private byte writeBuffer[] = new byte[8];
 
   /**
    * Creates a new data output stream to write data to the specified
@@ -130,8 +131,9 @@ public class LittleEndianDataOutputStream extends 
OutputStream {
    * @see java.io.FilterOutputStream#out
    */
   public final void writeShort(int v) throws IOException {
-    out.write((v >>> 0) & 0xFF);
-    out.write((v >>> 8) & 0xFF);
+    writeBuffer[0] = (byte) (v >>> 0);
+    writeBuffer[1] = (byte) (v >>> 8);
+    out.write(writeBuffer, 0, 2);
   }
 
   /**
@@ -144,17 +146,13 @@ public class LittleEndianDataOutputStream extends 
OutputStream {
    * @see java.io.FilterOutputStream#out
    */
   public final void writeInt(int v) throws IOException {
-    // TODO: see note in LittleEndianDataInputStream: maybe faster
-    // to use Integer.reverseBytes() and then writeInt, or a ByteBuffer
-    // approach
-    out.write((v >>> 0) & 0xFF);
-    out.write((v >>> 8) & 0xFF);
-    out.write((v >>> 16) & 0xFF);
-    out.write((v >>> 24) & 0xFF);
+    writeBuffer[0] = (byte) (v >>> 0);
+    writeBuffer[1] = (byte) (v >>> 8);
+    writeBuffer[2] = (byte) (v >>> 16);
+    writeBuffer[3] = (byte) (v >>> 24);
+    out.write(writeBuffer, 0, 4);
   }
 
-  private byte writeBuffer[] = new byte[8];
-
   /**
    * Writes a <code>long</code> to the underlying output stream as eight
    * bytes, low byte first. In no exception is thrown, the counter
diff --git 
a/parquet-common/src/test/java/org/apache/parquet/bytes/TestBytesInput.java 
b/parquet-common/src/test/java/org/apache/parquet/bytes/TestBytesInput.java
index 38d4b7921..6aca1811f 100644
--- a/parquet-common/src/test/java/org/apache/parquet/bytes/TestBytesInput.java
+++ b/parquet-common/src/test/java/org/apache/parquet/bytes/TestBytesInput.java
@@ -20,6 +20,7 @@ package org.apache.parquet.bytes;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -128,6 +129,28 @@ public class TestBytesInput {
     validate(data, factory);
   }
 
+  @Test
+  public void testFromByteArrayToByteArrayZeroCopy() throws IOException {
+    // Full array (offset=0, length=array.length): toByteArray() returns the 
backing array directly
+    byte[] data = new byte[1000];
+    RANDOM.nextBytes(data);
+    BytesInput bi = BytesInput.from(data, 0, data.length);
+    byte[] result = bi.toByteArray();
+    assertSame("toByteArray() should return the backing array when offset=0 
and length=full", data, result);
+  }
+
+  @Test
+  public void testFromByteArrayToByteArraySubRange() throws IOException {
+    // Sub-range (offset != 0): toByteArray() must return a copy of the 
specified range
+    byte[] input = new byte[1000];
+    RANDOM.nextBytes(input);
+    BytesInput bi = BytesInput.from(input, 10, 500);
+    byte[] result = bi.toByteArray();
+    byte[] expected = new byte[500];
+    System.arraycopy(input, 10, expected, 0, 500);
+    assertArrayEquals(expected, result);
+  }
+
   @Test
   public void testFromInputStream() throws IOException {
     byte[] data = new byte[1000];
diff --git 
a/parquet-encoding/src/test/java/org/apache/parquet/bytes/TestCapacityByteArrayOutputStream.java
 
b/parquet-encoding/src/test/java/org/apache/parquet/bytes/TestCapacityByteArrayOutputStream.java
index 583b90209..f27259469 100644
--- 
a/parquet-encoding/src/test/java/org/apache/parquet/bytes/TestCapacityByteArrayOutputStream.java
+++ 
b/parquet-encoding/src/test/java/org/apache/parquet/bytes/TestCapacityByteArrayOutputStream.java
@@ -300,4 +300,59 @@ public class TestCapacityByteArrayOutputStream {
       assertEquals(i, byteArray[i]);
     }
   }
+
+  // ---- Scalar write methods (writeInt, writeLong) ----
+
+  /**
+   * Reads a little-endian int from the byte array at position {@code pos}.
+   */
+  private static int readIntLE(byte[] b, int pos) {
+    return (b[pos] & 0xFF) | ((b[pos + 1] & 0xFF) << 8) | ((b[pos + 2] & 0xFF) 
<< 16) | ((b[pos + 3] & 0xFF) << 24);
+  }
+
+  /**
+   * Reads a little-endian long from the byte array at position {@code pos}.
+   */
+  private static long readLongLE(byte[] b, int pos) {
+    return (b[pos] & 0xFFL)
+        | ((b[pos + 1] & 0xFFL) << 8)
+        | ((b[pos + 2] & 0xFFL) << 16)
+        | ((b[pos + 3] & 0xFFL) << 24)
+        | ((b[pos + 4] & 0xFFL) << 32)
+        | ((b[pos + 5] & 0xFFL) << 40)
+        | ((b[pos + 6] & 0xFFL) << 48)
+        | ((b[pos + 7] & 0xFFL) << 56);
+  }
+
+  @Test
+  public void testWriteInt() throws Throwable {
+    try (CapacityByteArrayOutputStream cbaos = newCapacityBAOS(10)) {
+      int[] values = {0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE, 42};
+      for (int v : values) {
+        cbaos.writeInt(v);
+      }
+      assertEquals(values.length * 4, cbaos.size());
+
+      byte[] bytes = BytesInput.from(cbaos).toByteArray();
+      for (int i = 0; i < values.length; i++) {
+        assertEquals("value at index " + i, values[i], readIntLE(bytes, i * 
4));
+      }
+    }
+  }
+
+  @Test
+  public void testWriteLong() throws Throwable {
+    try (CapacityByteArrayOutputStream cbaos = newCapacityBAOS(10)) {
+      long[] values = {0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 
123456789L};
+      for (long v : values) {
+        cbaos.writeLong(v);
+      }
+      assertEquals(values.length * 8, cbaos.size());
+
+      byte[] bytes = BytesInput.from(cbaos).toByteArray();
+      for (int i = 0; i < values.length; i++) {
+        assertEquals("value at index " + i, values[i], readLongLE(bytes, i * 
8));
+      }
+    }
+  }
 }
diff --git 
a/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestColumnChunkPageWriteStore.java
 
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestColumnChunkPageWriteStore.java
index a17cf678f..8d735b55a 100644
--- 
a/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestColumnChunkPageWriteStore.java
+++ 
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestColumnChunkPageWriteStore.java
@@ -38,9 +38,8 @@ import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.inOrder;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.ByteOrder;
 import java.util.HashMap;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
@@ -49,7 +48,6 @@ import org.apache.parquet.bytes.ByteBufferAllocator;
 import org.apache.parquet.bytes.BytesInput;
 import org.apache.parquet.bytes.DirectByteBufferAllocator;
 import org.apache.parquet.bytes.HeapByteBufferAllocator;
-import org.apache.parquet.bytes.LittleEndianDataInputStream;
 import org.apache.parquet.bytes.TrackingByteBufferAllocator;
 import org.apache.parquet.column.ColumnDescriptor;
 import org.apache.parquet.column.Encoding;
@@ -249,12 +247,7 @@ public class TestColumnChunkPageWriteStore {
   }
 
   private int intValue(BytesInput in) throws IOException {
-    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    in.writeAllTo(baos);
-    LittleEndianDataInputStream os = new LittleEndianDataInputStream(new 
ByteArrayInputStream(baos.toByteArray()));
-    int i = os.readInt();
-    os.close();
-    return i;
+    return in.toByteBuffer().order(ByteOrder.LITTLE_ENDIAN).getInt();
   }
 
   @Test
diff --git a/pom.xml b/pom.xml
index 8a430a965..120d9089c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -609,6 +609,8 @@
               
<exclude>org.apache.parquet.internal.column.columnindex.IndexIterator</exclude>
               <!-- Removal of a protected method in a class that's not 
supposed to be subclassed by third-party code -->
               
<exclude>org.apache.parquet.column.values.bytestreamsplit.ByteStreamSplitValuesReader#gatherElementDataFromStreams(byte[])</exclude>
+              <!-- Removal of a protected internal field that should not have 
been part of the public API -->
+              
<exclude>org.apache.parquet.column.values.plain.PlainValuesReader#in</exclude>
               <!-- Due to the removal of deprecated methods -->
               
<exclude>org.apache.parquet.arrow.schema.SchemaMapping$TypeMappingVisitor#visit(org.apache.parquet.arrow.schema.SchemaMapping$MapTypeMapping)</exclude>
               <!-- Intentional removal of deprecated Pig support from 
parquet-thrift -->


Reply via email to