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

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 0fc240bf5f95 test: add Vortex reader/writer round-trip test (#19182)
0fc240bf5f95 is described below

commit 0fc240bf5f95d2fa979e3c8e01f66a65724abfe1
Author: Y Ethan Guo <[email protected]>
AuthorDate: Wed Jul 8 06:13:58 2026 -0700

    test: add Vortex reader/writer round-trip test (#19182)
    
    * feat: Add VORTEX file format enum, storage configs, and Maven dependency 
management
    
    Closes #18624
    
    * feat: Wire VORTEX into reader/writer factory dispatch and IOFactory
    
    Closes #18625
    
    * feat: Add Vortex base writer and Spark InternalRow writer
    
    Closes #18626
    
    * feat: Add Vortex Spark reader, record iterator, and resource closer
    
    Closes #18627
    
    * feat: Add Vortex Hive InputFormat stubs and wire Hive dispatch
    
    Mirror the Lance Hive integration for the VORTEX base file format:
    
    - Add HoodieVortexInputFormat (COW) and HoodieVortexRealtimeInputFormat
      (MOR) as catalog/metastore registration stubs; reading through the Hive
      InputFormat is not yet supported and throws, directing users to the Spark
      datasource path.
    - Wire VORTEX into HoodieInputFormatUtils dispatch: getInputFormat (by
      format and by extension), getInputFormatClassName, 
getOutputFormatClassName
      and getSerDeClassName (reusing the Parquet OutputFormat/SerDe like Lance).
    - Guard the Hive reader context so .vortex files fail fast with
      VORTEX_SPARK_ONLY_ERROR_MSG instead of being mis-read.
    
    VortexUtils (key iteration, schema reads, row counts, filterRowKeys) is
    already implemented at parity with LanceUtils.
    
    * feat: Add Vortex Spark datasource reader and wire into SparkAdapter
    
    Add the query-time Vortex reader for the Spark datasource path, mirroring 
the
    Lance integration for the initial Vortex scope (standard data types, no 
filter
    pushdown/data skipping, no BLOB/VECTOR handling).
    
    - Create SparkVortexReaderBase (hudi-spark-common) implementing
      SparkColumnarFileReader: opens the file via the dev.vortex.api Session, 
projects
      the requested columns, converts Arrow batches to rows through the existing
      VortexRecordIterator, and applies null-padding/cast projections plus 
partition
      column appending for schema evolution. Handles the partition-only / 
all-new-column
      case by emitting empty rows sized from DataSource.numRows().
    - Add SparkAdapter.createVortexFileReader; override in the Spark 
3.5/4.0/4.1/4.2
      adapters (Some) and 3.3/3.4 (None, connector artifact unavailable), 
matching the
      vortex-spark test matrix in the pom.
    - Extend MultipleColumnarFileFormatReader with a fourth vortexReader and a 
VORTEX
      dispatch case; thread it through HoodieFileGroupReaderBasedFileFormat,
      HoodieMergeOnReadRDDV2, and SparkReaderContextFactory (plus the 
single-format
      VORTEX branches) and update the file-group reader test constructor.
    
    * test: add Vortex writer and reader round-trip test
    
    Adds a file-level round trip through HoodieSparkVortexWriter and
    HoodieSparkVortexReader against the real dev.vortex.api: a multi-batch
    write+read of long/string/double columns and a projected (column-subset)
    read. Gated by -Dvortex.skip.tests.
    
    closes #18630
    
    * test: strengthen Vortex round-trip test per review
    
    - Wrap HoodieStorage in try-with-resources so it closes even when an 
assertion
      throws (previously closed only on the happy path).
    - Project the non-leading "name" column in testProjectedRead so the 
assertion
      actually pins projection; a leading-column ("id") projection read back at
      index 0 whether or not the reader narrowed to the requested schema.
    
    ---------
    
    Co-authored-by: voon <[email protected]>
---
 hudi-client/hudi-spark-client/pom.xml              |  44 +++
 .../hudi/io/storage/HoodieSparkVortexReader.java   | 205 ++++++++++++
 .../hudi/io/storage/HoodieSparkVortexWriter.java   | 259 ++++++++++++++++
 .../hudi/io/storage/VortexRecordIterator.java      | 199 ++++++++++++
 .../hudi/io/storage/VortexResourceCloser.java      |  57 ++++
 .../client/common/SparkReaderContextFactory.java   |   6 +-
 .../io/storage/HoodieSparkFileReaderFactory.java   |  11 +
 .../io/storage/HoodieSparkFileWriterFactory.java   |  22 ++
 .../hudi/MultipleColumnarFileFormatReader.scala    |  11 +-
 .../org/apache/spark/sql/hudi/SparkAdapter.scala   |  14 +
 .../hudi/common/config/HoodieStorageConfig.java    |  42 +++
 .../apache/hudi/common/model/HoodieFileFormat.java |  10 +-
 .../org/apache/hudi/common/util/VortexUtils.java   | 203 ++++++++++++
 .../core/io/storage/HoodieFileReaderFactory.java   |  10 +
 .../core/io/storage/HoodieFileWriterFactory.java   |  10 +
 .../hudi/core/io/storage/HoodieIOFactory.java      |   2 +
 hudi-hadoop-common/pom.xml                         |  44 +++
 .../hudi/io/vortex/HoodieBaseVortexWriter.java     | 345 +++++++++++++++++++++
 .../io/storage/hadoop/HoodieHadoopIOFactory.java   |   3 +
 .../hudi/hadoop/HiveHoodieReaderContext.java       |   3 +
 .../hudi/hadoop/HoodieVortexInputFormat.java       |  67 ++++
 .../realtime/HoodieVortexRealtimeInputFormat.java  |  64 ++++
 .../hudi/hadoop/utils/HoodieInputFormatUtils.java  |  23 ++
 hudi-spark-datasource/hudi-spark-common/pom.xml    |  44 +++
 .../datasources/vortex/SparkVortexReaderBase.scala | 203 ++++++++++++
 .../org/apache/hudi/HoodieMergeOnReadRDDV2.scala   |   5 +-
 .../HoodieFileGroupReaderBasedFileFormat.scala     |   5 +-
 hudi-spark-datasource/hudi-spark/pom.xml           |  43 +++
 .../storage/TestVortexReaderWriterRoundTrip.java   | 141 +++++++++
 .../read/TestHoodieFileGroupReaderOnSpark.scala    |   3 +-
 .../apache/spark/sql/adapter/Spark3_3Adapter.scala |   7 +
 .../apache/spark/sql/adapter/Spark3_4Adapter.scala |   8 +
 .../apache/spark/sql/adapter/Spark3_5Adapter.scala |   9 +
 .../apache/spark/sql/adapter/Spark4_0Adapter.scala |   8 +
 .../apache/spark/sql/adapter/Spark4_1Adapter.scala |   8 +
 .../apache/spark/sql/adapter/Spark4_2Adapter.scala |   8 +
 pom.xml                                            |  36 +++
 37 files changed, 2174 insertions(+), 8 deletions(-)

diff --git a/hudi-client/hudi-spark-client/pom.xml 
b/hudi-client/hudi-spark-client/pom.xml
index a54e33c9857a..21697a660957 100644
--- a/hudi-client/hudi-spark-client/pom.xml
+++ b/hudi-client/hudi-spark-client/pom.xml
@@ -300,4 +300,48 @@
       </resource>
     </resources>
   </build>
+
+  <profiles>
+    <!-- Vortex requires Java 17 (vortex-jni/vortex-spark ship Java 17 
bytecode). Compile the Vortex
+         reader/writer sources under src/main/java-vortex and pull the 
vortex-spark connector only on
+         JDK 17; Java 11 builds skip them. The factory dispatch 
(HoodieSparkFile{Reader,Writer}Factory)
+         instantiates these classes reflectively so it stays Java 11 
compilable. -->
+    <profile>
+      <id>vortex</id>
+      <activation>
+        <jdk>[17,)</jdk>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>dev.vortex</groupId>
+          <artifactId>${vortex.spark.artifact}</artifactId>
+        </dependency>
+      </dependencies>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-vortex-source</id>
+                <phase>generate-sources</phase>
+                <goals>
+                  <goal>add-source</goal>
+                </goals>
+                <configuration>
+                  <!-- Vortex is only supported on Spark 3.5+/4.x; skip the 
source on Spark 3.3/3.4
+                       (vortex.skip.tests=true) where its Spark 3.5+ APIs are 
unavailable. -->
+                  <skipAddSource>${vortex.skip.tests}</skipAddSource>
+                  <sources>
+                    <source>src/main/java-vortex</source>
+                  </sources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git 
a/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexReader.java
 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexReader.java
new file mode 100644
index 000000000000..d5cb85fd9bb8
--- /dev/null
+++ 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexReader.java
@@ -0,0 +1,205 @@
+/*
+ * 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.hudi.io.storage;
+
+import org.apache.hudi.HoodieSchemaConversionUtils;
+import org.apache.hudi.common.bloom.BloomFilter;
+import org.apache.hudi.common.config.HoodieStorageConfig;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.model.HoodieSparkRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaUtils;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.CloseableMappingIterator;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.exception.HoodieIOException;
+import org.apache.hudi.io.memory.HoodieArrowAllocator;
+import org.apache.hudi.storage.StoragePath;
+
+import dev.vortex.api.DataSource;
+import dev.vortex.api.Scan;
+import dev.vortex.api.ScanOptions;
+import dev.vortex.api.Session;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.catalyst.expressions.UnsafeRow;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.ArrowUtils$;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.apache.hudi.common.util.TypeUtils.unsafeCast;
+
+/**
+ * {@link HoodieSparkFileReader} implementation for Vortex file format, backed 
by the vortex-jni
+ * {@code dev.vortex.api} reader (Session / DataSource / Scan).
+ *
+ * <p>NOTE: vortex-jni 0.76.0 exposes no file-level custom-metadata API, so 
the Hudi bloom filter
+ * and min/max record-key footers are not available for Vortex files. {@link 
#readBloomFilter()}
+ * returns {@code null} and {@link #readMinMaxRecordKeys()} is unsupported.
+ * TODO(#18623): surface bloom/min-max footers once Vortex supports file 
metadata.
+ */
+@Slf4j
+public class HoodieSparkVortexReader implements HoodieSparkFileReader {
+  private final StoragePath path;
+  private final String uri;
+  private final long dataAllocatorSize;
+  // Session/DataSource are held so their native resources are not GC'd while 
this reader is open;
+  // neither is AutoCloseable in vortex-jni 0.76.0 (cleanup is implicit).
+  private final Session metadataSession;
+  private final DataSource metadataDataSource;
+  private final BufferAllocator metadataAllocator;
+
+  public HoodieSparkVortexReader(StoragePath path) {
+    this(path, 
Long.parseLong(HoodieStorageConfig.VORTEX_READ_ALLOCATOR_SIZE_BYTES.defaultValue()));
+  }
+
+  public HoodieSparkVortexReader(StoragePath path, long dataAllocatorSize) {
+    this.path = path;
+    this.uri = toVortexUri(path);
+    this.dataAllocatorSize = dataAllocatorSize;
+    this.metadataAllocator = HoodieArrowAllocator.newChildAllocator(
+        getClass().getSimpleName() + "-meta-" + path.getName(),
+        
Long.parseLong(HoodieStorageConfig.VORTEX_READ_METADATA_ALLOCATOR_SIZE_BYTES.defaultValue()));
+    try {
+      this.metadataSession = Session.create();
+      this.metadataDataSource = DataSource.open(metadataSession, uri);
+    } catch (Exception e) {
+      metadataAllocator.close();
+      throw new HoodieException("Failed to open Vortex file: " + path, e);
+    }
+  }
+
+  @Override
+  public String[] readMinMaxRecordKeys() {
+    throw new UnsupportedOperationException(
+        "Min/max record keys are not available for Vortex files (no 
file-metadata support in vortex-jni 0.76.0)");
+  }
+
+  @Override
+  public BloomFilter readBloomFilter() {
+    // Vortex files carry no Hudi bloom filter footer yet; callers treat null 
as "no bloom filter".
+    return null;
+  }
+
+  @Override
+  public Set<Pair<String, Long>> filterRowKeys(Set<String> candidateRowKeys) {
+    Set<Pair<String, Long>> result = new HashSet<>();
+    long position = 0;
+
+    try (ClosableIterator<String> keyIterator = getRecordKeyIterator()) {
+      while (keyIterator.hasNext()) {
+        String recordKey = keyIterator.next();
+        if (candidateRowKeys == null || candidateRowKeys.isEmpty()
+                || candidateRowKeys.contains(recordKey)) {
+          result.add(Pair.of(recordKey, position));
+        }
+        position++;
+      }
+    } catch (IOException e) {
+      throw new HoodieIOException("Failed to filter row keys from Vortex file: 
" + path, e);
+    }
+
+    return result;
+  }
+
+  @Override
+  public ClosableIterator<HoodieRecord<InternalRow>> 
getRecordIterator(HoodieSchema readerSchema, HoodieSchema requestedSchema) 
throws IOException {
+    return getRecordIterator(requestedSchema);
+  }
+
+  @Override
+  public ClosableIterator<HoodieRecord<InternalRow>> 
getRecordIterator(HoodieSchema schema) throws IOException {
+    ClosableIterator<UnsafeRow> iterator = getUnsafeRowIterator(schema);
+    return new CloseableMappingIterator<>(iterator, data -> unsafeCast(new 
HoodieSparkRecord(data)));
+  }
+
+  @Override
+  public ClosableIterator<String> getRecordKeyIterator() throws IOException {
+    HoodieSchema recordKeySchema = HoodieSchemaUtils.getRecordKeySchema();
+    ClosableIterator<UnsafeRow> iterator = 
getUnsafeRowIterator(recordKeySchema);
+    return new CloseableMappingIterator<>(iterator, data -> 
data.getUTF8String(0).toString());
+  }
+
+  private ClosableIterator<UnsafeRow> getUnsafeRowIterator(HoodieSchema 
requestedSchema) {
+    StructType requestedSparkSchema = 
HoodieSchemaConversionUtils.convertHoodieSchemaToStructType(requestedSchema);
+
+    BufferAllocator allocator = HoodieArrowAllocator.newChildAllocator(
+        getClass().getSimpleName() + "-data-" + path.getName(), 
dataAllocatorSize);
+
+    try {
+      Session session = Session.create();
+      DataSource dataSource = DataSource.open(session, uri);
+      // Read all columns; VortexRecordIterator projects the requested columns 
by name.
+      Scan scan = dataSource.scan(ScanOptions.of());
+      return new VortexRecordIterator(allocator, session, dataSource, scan, 
requestedSparkSchema, uri);
+    } catch (Exception e) {
+      allocator.close();
+      throw new HoodieException("Failed to create Vortex reader for: " + path, 
e);
+    }
+  }
+
+  @Override
+  public HoodieSchema getSchema() {
+    try {
+      Schema arrowSchema = metadataDataSource.arrowSchema(metadataAllocator);
+      StructType structType = (StructType) 
ArrowUtils$.MODULE$.fromArrowSchema(arrowSchema);
+      return 
HoodieSchemaConversionUtils.convertStructTypeToHoodieSchema(structType, 
"record", "", false);
+    } catch (Exception e) {
+      throw new HoodieException("Failed to read schema from Vortex file: " + 
path, e);
+    }
+  }
+
+  @Override
+  public void close() {
+    try {
+      metadataAllocator.close();
+    } catch (Exception e) {
+      log.warn("Error while closing Vortex metadata allocator: {}", 
e.getMessage());
+    }
+  }
+
+  @Override
+  public long getTotalRecords() {
+    try {
+      return metadataDataSource.rowCount().asOptional().orElseThrow(
+          () -> new HoodieException("Vortex row count unavailable for file: " 
+ path));
+    } catch (Exception e) {
+      throw new HoodieException("Failed to get row count from Vortex file: " + 
path, e);
+    }
+  }
+
+  /**
+   * Vortex requires a well-formed URL. Remote-store {@link StoragePath}s 
already carry a scheme
+   * (s3://, hdfs://, ...); local paths may not, so qualify them as {@code 
file://} URIs.
+   */
+  private static String toVortexUri(StoragePath path) {
+    URI parsed = path.toUri();
+    if (parsed.getScheme() == null) {
+      return new java.io.File(parsed.getPath()).toURI().toString();
+    }
+    return parsed.toString();
+  }
+}
diff --git 
a/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexWriter.java
 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexWriter.java
new file mode 100644
index 000000000000..4467c30d2496
--- /dev/null
+++ 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/HoodieSparkVortexWriter.java
@@ -0,0 +1,259 @@
+/*
+ * 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.hudi.io.storage;
+
+import org.apache.hudi.common.bloom.BloomFilter;
+import org.apache.hudi.common.config.HoodieStorageConfig;
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.model.HoodieKey;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.io.storage.row.HoodieBloomFilterRowWriteSupport;
+import org.apache.hudi.io.storage.row.HoodieInternalRowFileWriter;
+import org.apache.hudi.io.vortex.HoodieBaseVortexWriter;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.execution.arrow.ArrowWriter$;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.ArrowUtils$;
+import org.apache.spark.unsafe.types.UTF8String;
+
+import java.io.IOException;
+import java.util.function.Function;
+
+import static 
org.apache.hudi.common.model.HoodieRecord.HoodieMetadataField.COMMIT_SEQNO_METADATA_FIELD;
+import static 
org.apache.hudi.common.model.HoodieRecord.HoodieMetadataField.COMMIT_TIME_METADATA_FIELD;
+import static 
org.apache.hudi.common.model.HoodieRecord.HoodieMetadataField.FILENAME_METADATA_FIELD;
+import static 
org.apache.hudi.common.model.HoodieRecord.HoodieMetadataField.PARTITION_PATH_METADATA_FIELD;
+import static 
org.apache.hudi.common.model.HoodieRecord.HoodieMetadataField.RECORD_KEY_METADATA_FIELD;
+import static org.apache.hudi.common.util.ValidationUtils.checkArgument;
+
+/**
+ * Spark Vortex file writer implementing {@link HoodieSparkFileWriter} and 
{@link HoodieInternalRowFileWriter}.
+ *
+ * This writer integrates with Hudi's storage I/O layer and supports:
+ * - Hudi metadata field population
+ * - Record key tracking (for bloom filters)
+ * - Sequence ID generation
+ * - Min/max record key tracking
+ */
+public class HoodieSparkVortexWriter extends 
HoodieBaseVortexWriter<InternalRow, UTF8String>
+    implements HoodieSparkFileWriter, HoodieInternalRowFileWriter {
+
+  private static final String DEFAULT_TIMEZONE = "UTC";
+  private static final long MIN_RECORDS_FOR_SIZE_CHECK = 100L;
+  private static final long MAX_RECORDS_FOR_SIZE_CHECK = 10000L;
+
+  private final StructType sparkSchema;
+  private final Schema arrowSchema;
+  private final UTF8String fileName;
+  private final UTF8String instantTime;
+  private final boolean populateMetaFields;
+  private final Function<Long, String> seqIdGenerator;
+  private final long maxFileSize;
+  private long recordCountForNextSizeCheck = MIN_RECORDS_FOR_SIZE_CHECK;
+
+  @Builder(builderMethodName = "builder")
+  private static HoodieSparkVortexWriter create(
+      StoragePath file,
+      StructType sparkSchema,
+      String instantTime,
+      TaskContextSupplier taskContextSupplier,
+      HoodieStorage storage,
+      boolean populateMetaFields,
+      Option<BloomFilter> bloomFilterOpt,
+      long maxFileSize,
+      long allocatorSize,
+      long flushByteWatermark) {
+    checkArgument(maxFileSize > 0, "maxFileSize must be a positive number");
+    checkArgument(allocatorSize > 0, "allocatorSize must be a positive 
number");
+    checkArgument(flushByteWatermark > 0, "flushByteWatermark must be a 
positive number");
+    checkArgument(flushByteWatermark < allocatorSize,
+        "flushByteWatermark (" + flushByteWatermark + ") must be less than 
allocatorSize ("
+            + allocatorSize + ") so the byte-aware flush prevents 
reallocations from exceeding the cap");
+    return new HoodieSparkVortexWriter(file, sparkSchema, instantTime,
+        taskContextSupplier, storage, populateMetaFields, bloomFilterOpt, 
maxFileSize,
+        allocatorSize, flushByteWatermark);
+  }
+
+  /**
+   * Public reflection entry point for {@code HoodieSparkFileWriterFactory}. 
The factory lives in the
+   * default (Java 11) source set and cannot reference this Vortex-only class 
directly, so it invokes
+   * this method reflectively (see the vortex profile that compiles this 
source under JDK 17).
+   */
+  public static HoodieSparkVortexWriter newWriter(
+      StoragePath file,
+      StructType sparkSchema,
+      String instantTime,
+      TaskContextSupplier taskContextSupplier,
+      HoodieStorage storage,
+      boolean populateMetaFields,
+      Option<BloomFilter> bloomFilterOpt,
+      long maxFileSize,
+      long allocatorSize,
+      long flushByteWatermark) {
+    return builder()
+        .file(file)
+        .sparkSchema(sparkSchema)
+        .instantTime(instantTime)
+        .taskContextSupplier(taskContextSupplier)
+        .storage(storage)
+        .populateMetaFields(populateMetaFields)
+        .bloomFilterOpt(bloomFilterOpt)
+        .maxFileSize(maxFileSize)
+        .allocatorSize(allocatorSize)
+        .flushByteWatermark(flushByteWatermark)
+        .build();
+  }
+
+  /**
+   * Manually declared builder class to provide default values for optional 
parameters.
+   * Lombok fills in the remaining builder methods.
+   */
+  public static class HoodieSparkVortexWriterBuilder {
+    private Option<BloomFilter> bloomFilterOpt = Option.empty();
+    private long maxFileSize = 
Long.parseLong(HoodieStorageConfig.VORTEX_MAX_FILE_SIZE.defaultValue());
+    private long allocatorSize = 
Long.parseLong(HoodieStorageConfig.VORTEX_WRITE_ALLOCATOR_SIZE_BYTES.defaultValue());
+    private long flushByteWatermark = 
Long.parseLong(HoodieStorageConfig.VORTEX_WRITE_FLUSH_BYTE_WATERMARK.defaultValue());
+  }
+
+  private HoodieSparkVortexWriter(StoragePath file,
+                                  StructType sparkSchema,
+                                  String instantTime,
+                                  TaskContextSupplier taskContextSupplier,
+                                  HoodieStorage storage,
+                                  boolean populateMetaFields,
+                                  Option<BloomFilter> bloomFilterOpt,
+                                  long maxFileSize,
+                                  long allocatorSize,
+                                  long flushByteWatermark) {
+    super(file, DEFAULT_BATCH_SIZE, allocatorSize, flushByteWatermark,
+        bloomFilterOpt.map(HoodieBloomFilterRowWriteSupport::new));
+    this.sparkSchema = sparkSchema;
+    this.arrowSchema = ArrowUtils$.MODULE$.toArrowSchema(sparkSchema, 
DEFAULT_TIMEZONE, false, false);
+    this.fileName = UTF8String.fromString(file.getName());
+    this.instantTime = UTF8String.fromString(instantTime);
+    this.populateMetaFields = populateMetaFields;
+    this.maxFileSize = maxFileSize;
+    this.seqIdGenerator = recordIndex -> {
+      Integer partitionId = taskContextSupplier.getPartitionIdSupplier().get();
+      return HoodieRecord.generateSequenceId(instantTime, partitionId, 
recordIndex);
+    };
+  }
+
+  @Override
+  public void writeRowWithMetadata(HoodieKey key, InternalRow row) throws 
IOException {
+    UTF8String recordKey = UTF8String.fromString(key.getRecordKey());
+    bloomFilterWriteSupportOpt.ifPresent(bloomFilterWriteSupport -> 
bloomFilterWriteSupport.addKey(recordKey));
+    if (populateMetaFields) {
+      updateRecordMetadata(row, recordKey, key.getPartitionPath(), 
getWrittenRecordCount());
+    }
+    super.write(row);
+  }
+
+  @Override
+  public void writeRow(String recordKey, InternalRow row) throws IOException {
+    bloomFilterWriteSupportOpt.ifPresent(bloomFilterWriteSupport ->
+        bloomFilterWriteSupport.addKey(UTF8String.fromString(recordKey)));
+    super.write(row);
+  }
+
+  @Override
+  public void writeRow(UTF8String key, InternalRow row) throws IOException {
+    bloomFilterWriteSupportOpt.ifPresent(bloomFilterWriteSupport -> 
bloomFilterWriteSupport.addKey(key));
+    super.write(row);
+  }
+
+  @Override
+  public void writeRow(InternalRow row) throws IOException {
+    super.write(row);
+  }
+
+  @Override
+  protected HoodieBaseVortexWriter.ArrowWriter<InternalRow> 
createArrowWriter(VectorSchemaRoot root) {
+    return SparkArrowWriterAdapter.of(ArrowWriter$.MODULE$.create(root));
+  }
+
+  /**
+   * Check if writer can accept more records based on estimated data size.
+   *
+   * @return true if writer can accept more records, false if file size limit 
is reached
+   */
+  public boolean canWrite() {
+    long writtenCount = getWrittenRecordCount();
+    if (writtenCount >= recordCountForNextSizeCheck) {
+      long dataSize = getDataSize();
+      long avgRecordSize = Math.max(dataSize / writtenCount, 1);
+      if (dataSize > (maxFileSize - avgRecordSize * 2)) {
+        return false;
+      }
+      recordCountForNextSizeCheck = writtenCount + Math.min(
+          Math.max(MIN_RECORDS_FOR_SIZE_CHECK, (maxFileSize / avgRecordSize - 
writtenCount) / 2),
+          MAX_RECORDS_FOR_SIZE_CHECK);
+    }
+    return true;
+  }
+
+  @Override
+  protected Schema getArrowSchema() {
+    return arrowSchema;
+  }
+
+  /**
+   * Update Hudi metadata fields in the InternalRow.
+   */
+  protected void updateRecordMetadata(InternalRow row,
+                                      UTF8String recordKey,
+                                      String partitionPath,
+                                      long recordCount) {
+    row.update(COMMIT_TIME_METADATA_FIELD.ordinal(), instantTime);
+    row.update(COMMIT_SEQNO_METADATA_FIELD.ordinal(), 
UTF8String.fromString(seqIdGenerator.apply(recordCount)));
+    row.update(RECORD_KEY_METADATA_FIELD.ordinal(), recordKey);
+    row.update(PARTITION_PATH_METADATA_FIELD.ordinal(), 
UTF8String.fromString(partitionPath));
+    row.update(FILENAME_METADATA_FIELD.ordinal(), fileName);
+  }
+
+  @AllArgsConstructor(staticName = "of")
+  private static class SparkArrowWriterAdapter implements 
HoodieBaseVortexWriter.ArrowWriter<InternalRow> {
+    // Fully qualified: the inherited HoodieBaseVortexWriter.ArrowWriter 
nested type would otherwise
+    // shadow this Spark ArrowWriter import.
+    private final org.apache.spark.sql.execution.arrow.ArrowWriter 
sparkArrowWriter;
+
+    @Override
+    public void write(InternalRow row) {
+      sparkArrowWriter.write(row);
+    }
+
+    @Override
+    public void reset() {
+      sparkArrowWriter.reset();
+    }
+
+    @Override
+    public void finishBatch() {
+      sparkArrowWriter.finish();
+    }
+  }
+}
diff --git 
a/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexRecordIterator.java
 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexRecordIterator.java
new file mode 100644
index 000000000000..85724f05a936
--- /dev/null
+++ 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexRecordIterator.java
@@ -0,0 +1,199 @@
+/*
+ * 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.hudi.io.storage;
+
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.exception.HoodieException;
+
+import dev.vortex.api.DataSource;
+import dev.vortex.api.Partition;
+import dev.vortex.api.Scan;
+import dev.vortex.api.Session;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.catalyst.expressions.UnsafeProjection;
+import org.apache.spark.sql.catalyst.expressions.UnsafeRow;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.vectorized.ArrowColumnVector;
+import org.apache.spark.sql.vectorized.ColumnVector;
+import org.apache.spark.sql.vectorized.ColumnarBatch;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Iterator for reading Vortex files (vortex-jni 0.76.0) and converting Arrow 
batches to Spark
+ * {@link UnsafeRow}s.
+ *
+ * <p>A {@link Scan} yields a sequence of {@link Partition}s; each partition 
is read as an Arrow
+ * {@link ArrowReader} via {@link Partition#scanArrow}, whose {@link 
VectorSchemaRoot} is reused
+ * across {@code loadNextBatch} calls. Only the columns present in the 
requested Spark schema are
+ * projected (by name) out of each batch, so the scan may read more columns 
than requested.
+ *
+ * <p>Lifecycle managed here: the {@link BufferAllocator} and the current 
{@link ArrowReader}. The
+ * {@link Session}/{@link DataSource}/{@link Scan} handles are held only to 
keep their native
+ * resources alive during iteration (they are not AutoCloseable in vortex-jni 
0.76.0).
+ */
+public final class VortexRecordIterator implements ClosableIterator<UnsafeRow> 
{
+  private final BufferAllocator allocator;
+  private final Session session;
+  private final DataSource dataSource;
+  private final Scan scan;
+  private final StructType sparkSchema;
+  private final UnsafeProjection projection;
+  private final String path;
+
+  private ArrowReader currentReader;
+  private ColumnVector[] columnVectors;
+  private ColumnarBatch currentBatch;
+  private Iterator<InternalRow> rowIterator;
+  private boolean closed = false;
+
+  public VortexRecordIterator(BufferAllocator allocator,
+                              Session session,
+                              DataSource dataSource,
+                              Scan scan,
+                              StructType schema,
+                              String path) {
+    this.allocator = allocator;
+    this.session = session;
+    this.dataSource = dataSource;
+    this.scan = scan;
+    this.sparkSchema = schema;
+    this.projection = UnsafeProjection.create(schema);
+    this.path = path;
+  }
+
+  @Override
+  public boolean hasNext() {
+    if (rowIterator != null && rowIterator.hasNext()) {
+      return true;
+    }
+
+    // Advance within the current partition, then across partitions, until a 
non-empty batch.
+    while (true) {
+      if (currentReader != null) {
+        if (advanceBatch()) {
+          return true;
+        }
+        // Current partition exhausted.
+        closeCurrentReader();
+      }
+      if (!scan.hasNext()) {
+        return false;
+      }
+      Partition partition = scan.next();
+      currentReader = partition.scanArrow(allocator);
+      columnVectors = null;
+    }
+  }
+
+  /**
+   * Load the next batch from the current ArrowReader (whose VectorSchemaRoot 
is reused across
+   * batches, so the wrapping column vectors are built once per partition) and 
set up the row
+   * iterator. Returns false when the current reader is exhausted.
+   */
+  private boolean advanceBatch() {
+    try {
+      while (currentReader.loadNextBatch()) {
+        VectorSchemaRoot root = currentReader.getVectorSchemaRoot();
+        if (columnVectors == null) {
+          columnVectors = buildColumnVectors(root);
+        }
+        currentBatch = new ColumnarBatch(columnVectors, root.getRowCount());
+        rowIterator = currentBatch.rowIterator();
+        if (rowIterator.hasNext()) {
+          return true;
+        }
+      }
+    } catch (IOException e) {
+      throw new HoodieException("Failed to read next batch from Vortex file: " 
+ path, e);
+    }
+    return false;
+  }
+
+  @Override
+  public UnsafeRow next() {
+    if (!hasNext()) {
+      throw new IllegalStateException("No more records available");
+    }
+    InternalRow row = rowIterator.next();
+    return projection.apply(row).copy();
+  }
+
+  /**
+   * Wrap the requested columns of the batch as Spark {@link ColumnVector}s, 
matched by name. The
+   * batch may carry more columns than requested (the scan reads all columns); 
extras are ignored.
+   */
+  private ColumnVector[] buildColumnVectors(VectorSchemaRoot root) {
+    List<FieldVector> fieldVectors = root.getFieldVectors();
+    Map<String, FieldVector> byName = new HashMap<>(fieldVectors.size() * 2);
+    for (FieldVector fv : fieldVectors) {
+      byName.put(fv.getName(), fv);
+    }
+    StructField[] sparkFields = sparkSchema.fields();
+    ColumnVector[] vectors = new ColumnVector[sparkFields.length];
+    for (int i = 0; i < sparkFields.length; i++) {
+      String name = sparkFields[i].name();
+      FieldVector fv = byName.get(name);
+      if (fv == null) {
+        throw new HoodieException("Vortex batch missing expected column '" + 
name
+            + "' for file: " + path + "; available columns: " + 
byName.keySet());
+      }
+      vectors[i] = new ArrowColumnVector(fv);
+    }
+    return vectors;
+  }
+
+  private void closeCurrentReader() {
+    currentBatch = null;
+    columnVectors = null;
+    rowIterator = null;
+    if (currentReader != null) {
+      try {
+        currentReader.close();
+      } catch (IOException e) {
+        throw new HoodieException("Failed to close Vortex Arrow reader for 
file: " + path, e);
+      } finally {
+        currentReader = null;
+      }
+    }
+  }
+
+  @Override
+  public void close() {
+    if (closed) {
+      return;
+    }
+    closed = true;
+    ArrowReader reader = currentReader;
+    currentReader = null;
+    currentBatch = null;
+    columnVectors = null;
+    rowIterator = null;
+    VortexResourceCloser.closeAll(reader, allocator, path);
+  }
+}
diff --git 
a/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexResourceCloser.java
 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexResourceCloser.java
new file mode 100644
index 000000000000..afe37170b249
--- /dev/null
+++ 
b/hudi-client/hudi-spark-client/src/main/java-vortex/org/apache/hudi/io/storage/VortexResourceCloser.java
@@ -0,0 +1,57 @@
+/*
+ * 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.hudi.io.storage;
+
+import org.apache.hudi.exception.HoodieException;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.ipc.ArrowReader;
+
+/**
+ * Shared close logic for the Vortex reader iterator (vortex-jni 0.76.0). 
Closes the current Arrow
+ * reader (which owns the exported VectorSchemaRoot / column vectors) and then 
the allocator,
+ * rethrowing after the allocator is closed so a failed reader close never 
leaks the allocator.
+ *
+ * <p>The Vortex Session/DataSource/Scan handles are not AutoCloseable in 
0.76.0 (native cleanup is
+ * implicit), so they are not closed here.
+ */
+final class VortexResourceCloser {
+
+  private VortexResourceCloser() {
+  }
+
+  static void closeAll(ArrowReader currentReader, BufferAllocator allocator, 
String path) {
+    Exception readerException = null;
+
+    if (currentReader != null) {
+      try {
+        currentReader.close();
+      } catch (Exception e) {
+        readerException = e;
+      }
+    }
+    if (allocator != null) {
+      allocator.close();
+    }
+
+    if (readerException != null) {
+      throw new HoodieException("Failed to close Vortex Arrow reader for file: 
" + path, readerException);
+    }
+  }
+}
diff --git 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/common/SparkReaderContextFactory.java
 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/common/SparkReaderContextFactory.java
index 22a21369b3c6..bb54f9cc1fc0 100644
--- 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/common/SparkReaderContextFactory.java
+++ 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/common/SparkReaderContextFactory.java
@@ -97,13 +97,17 @@ public class SparkReaderContextFactory implements 
ReaderContextFactory<InternalR
       SparkColumnarFileReader parquetFileReader = 
sparkAdapter.createParquetFileReader(false, sqlConf, options, configs);
       SparkColumnarFileReader orcFileReader = getOrcFileReader(resolver, 
sqlConf, options, configs, sparkAdapter);
       SparkColumnarFileReader lanceFileReader = 
sparkAdapter.createLanceFileReader(false, sqlConf, options, 
configs).getOrElse(null);
-      baseFileReaderBroadcast = jsc.broadcast(new 
MultipleColumnarFileFormatReader(parquetFileReader, orcFileReader, 
lanceFileReader));
+      SparkColumnarFileReader vortexFileReader = 
sparkAdapter.createVortexFileReader(false, sqlConf, options, 
configs).getOrElse(null);
+      baseFileReaderBroadcast = jsc.broadcast(new 
MultipleColumnarFileFormatReader(parquetFileReader, orcFileReader, 
lanceFileReader, vortexFileReader));
     } else if (metaClient.getTableConfig().getBaseFileFormat() == 
HoodieFileFormat.ORC) {
       SparkColumnarFileReader orcFileReader = getOrcFileReader(resolver, 
sqlConf, options, configs, sparkAdapter);
       baseFileReaderBroadcast = jsc.broadcast(orcFileReader);
     } else if (metaClient.getTableConfig().getBaseFileFormat() == 
HoodieFileFormat.LANCE) {
       baseFileReaderBroadcast = jsc.broadcast(
           sparkAdapter.createLanceFileReader(false, sqlConf, options, 
configs).getOrElse(null));
+    } else if (metaClient.getTableConfig().getBaseFileFormat() == 
HoodieFileFormat.VORTEX) {
+      baseFileReaderBroadcast = jsc.broadcast(
+          sparkAdapter.createVortexFileReader(false, sqlConf, options, 
configs).getOrElse(null));
     } else {
       baseFileReaderBroadcast = jsc.broadcast(
           sparkAdapter.createParquetFileReader(false, sqlConf, options, 
configs));
diff --git 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileReaderFactory.java
 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileReaderFactory.java
index c9be89ab038c..c1d6bed8266f 100644
--- 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileReaderFactory.java
+++ 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileReaderFactory.java
@@ -22,6 +22,7 @@ import org.apache.hudi.common.config.HoodieConfig;
 import org.apache.hudi.common.config.HoodieStorageConfig;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.ReflectionUtils;
 import org.apache.hudi.core.io.storage.HoodieFileReader;
 import org.apache.hudi.core.io.storage.HoodieFileReaderFactory;
 import org.apache.hudi.exception.HoodieIOException;
@@ -58,6 +59,16 @@ public class HoodieSparkFileReaderFactory extends 
HoodieFileReaderFactory {
     return new HoodieSparkLanceReader(path, dataAllocatorSize, 
metadataAllocatorSize);
   }
 
+  @Override
+  protected HoodieFileReader newVortexFileReader(HoodieConfig hoodieConfig, 
StoragePath path) {
+    long dataAllocatorSize = 
hoodieConfig.getLongOrDefault(HoodieStorageConfig.VORTEX_READ_ALLOCATOR_SIZE_BYTES);
+    // Vortex sources are compiled only under JDK 17 (see the vortex profile 
in this module's pom),
+    // so instantiate the reader reflectively to keep this factory compilable 
on the Java 11 build.
+    return (HoodieFileReader) ReflectionUtils.loadClass(
+        "org.apache.hudi.io.storage.HoodieSparkVortexReader",
+        new Class<?>[] {StoragePath.class, long.class}, path, 
dataAllocatorSize);
+  }
+
   @Override
   protected HoodieFileReader newHFileFileReader(HoodieConfig hoodieConfig,
                                                 StoragePath path,
diff --git 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileWriterFactory.java
 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileWriterFactory.java
index 43264705885e..67949fee472e 100644
--- 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileWriterFactory.java
+++ 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkFileWriterFactory.java
@@ -25,6 +25,7 @@ import org.apache.hudi.common.engine.TaskContextSupplier;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.table.HoodieTableConfig;
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.ReflectionUtils;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.core.io.HoodieParquetConfigInjector;
 import org.apache.hudi.core.io.storage.HoodieFileWriter;
@@ -140,6 +141,27 @@ public class HoodieSparkFileWriterFactory extends 
HoodieFileWriterFactory {
         .build();
   }
 
+  @Override
+  protected HoodieFileWriter newVortexFileWriter(String instantTime, 
StoragePath path, HoodieConfig config, HoodieSchema schema,
+                                                 TaskContextSupplier 
taskContextSupplier) throws IOException {
+    boolean populateMetaFields = 
config.getBooleanOrDefault(HoodieTableConfig.POPULATE_META_FIELDS);
+    StructType structType = HoodieInternalRowUtils.getCachedSchema(schema);
+    boolean enableBloomFilter = enableBloomFilter(populateMetaFields, config);
+    Option<BloomFilter> bloomFilter = enableBloomFilter ? 
Option.of(createBloomFilter(config)) : Option.empty();
+    long maxFileSize = 
config.getLongOrDefault(HoodieStorageConfig.VORTEX_MAX_FILE_SIZE);
+    long allocatorSize = 
config.getLongOrDefault(HoodieStorageConfig.VORTEX_WRITE_ALLOCATOR_SIZE_BYTES);
+    long flushByteWatermark = 
config.getLongOrDefault(HoodieStorageConfig.VORTEX_WRITE_FLUSH_BYTE_WATERMARK);
+
+    // Vortex sources are compiled only under JDK 17 (see the vortex profile 
in this module's pom),
+    // so build the writer reflectively to keep this factory compilable on the 
Java 11 build.
+    return (HoodieFileWriter) ReflectionUtils.invokeStaticMethod(
+        "org.apache.hudi.io.storage.HoodieSparkVortexWriter", "newWriter",
+        new Object[] {path, structType, instantTime, taskContextSupplier, 
storage, populateMetaFields,
+            bloomFilter, maxFileSize, allocatorSize, flushByteWatermark},
+        StoragePath.class, StructType.class, String.class, 
TaskContextSupplier.class, HoodieStorage.class,
+        boolean.class, Option.class, long.class, long.class, long.class);
+  }
+
   private static HoodieRowParquetWriteSupport 
getHoodieRowParquetWriteSupport(StorageConfiguration<?> conf, HoodieSchema 
schema,
                                                                               
HoodieConfig config, boolean enableBloomFilter) {
     Option<BloomFilter> filter = enableBloomFilter ? 
Option.of(createBloomFilter(config)) : Option.empty();
diff --git 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/MultipleColumnarFileFormatReader.scala
 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/MultipleColumnarFileFormatReader.scala
index e73378f8819e..9bce5fcb55f3 100644
--- 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/MultipleColumnarFileFormatReader.scala
+++ 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/MultipleColumnarFileFormatReader.scala
@@ -29,12 +29,12 @@ import 
org.apache.spark.sql.execution.datasources.{PartitionedFile, SparkColumna
 import org.apache.spark.sql.sources.Filter
 import org.apache.spark.sql.types.StructType
 
-class MultipleColumnarFileFormatReader(parquetReader: SparkColumnarFileReader, 
orcReader: SparkColumnarFileReader, lanceReader: SparkColumnarFileReader)
+class MultipleColumnarFileFormatReader(parquetReader: SparkColumnarFileReader, 
orcReader: SparkColumnarFileReader, lanceReader: SparkColumnarFileReader, 
vortexReader: SparkColumnarFileReader)
   extends SparkColumnarFileReader with SparkAdapterSupport {
 
   def this(parquetReader: SparkColumnarFileReader, orcReader: 
SparkColumnarFileReader) = {
-    // constructor for Spark versions without Lance support
-    this(parquetReader, orcReader, null)
+    // constructor for Spark versions without Lance or Vortex support
+    this(parquetReader, orcReader, null, null)
   }
 
   /**
@@ -63,6 +63,11 @@ class MultipleColumnarFileFormatReader(parquetReader: 
SparkColumnarFileReader, o
           throw new UnsupportedOperationException("Lance format is only 
supported in Spark 3.4 and above")
         }
         lanceReader.read(file, requiredSchema, partitionSchema, 
internalSchemaOpt, filters, storageConf, tableSchemaOpt)
+      case HoodieFileFormat.VORTEX =>
+        if (vortexReader == null) {
+          throw new UnsupportedOperationException("Vortex format is only 
supported in Spark 3.5 and above")
+        }
+        vortexReader.read(file, requiredSchema, partitionSchema, 
internalSchemaOpt, filters, storageConf, tableSchemaOpt)
       case _ =>
         throw new IllegalArgumentException(s"Unsupported file format for file: 
$filePath")
     }
diff --git 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
index 2934536f4ec9..7cc32490fbfb 100644
--- 
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
+++ 
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/hudi/SparkAdapter.scala
@@ -243,6 +243,20 @@ trait SparkAdapter extends Serializable {
                             options: Map[String, String],
                             hadoopConf: Configuration): 
Option[SparkColumnarFileReader]
 
+  /**
+   * Get Vortex file reader
+   *
+   * @param vectorized true if vectorized reading is not prohibited due to 
schema, reading mode, etc
+   * @param sqlConf    the [[SQLConf]] used for the read
+   * @param options    passed as a param to the file format
+   * @param hadoopConf some configs will be set for the hadoopConf
+   * @return Vortex file reader wrapped in Option; None if Vortex format is 
not supported in current Spark version (i.e 3.3, 3.4)
+   */
+  def createVortexFileReader(vectorized: Boolean,
+                             sqlConf: SQLConf,
+                             options: Map[String, String],
+                             hadoopConf: Configuration): 
Option[SparkColumnarFileReader]
+
   /**
    * Build the [[HoodieParquetReadSupport]] for a parquet read. Spark 4.0 
overrides to return
    * its variant-aware subclass (variant group field reorder for the 
positional converter).
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java
index e284e8030882..60e062da2c4d 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java
@@ -132,6 +132,43 @@ public class HoodieStorageConfig extends HoodieConfig {
       .sinceVersion("1.3.0")
       .withDocumentation("Control whether to write bloom filter or not to 
HFile.");
 
+  public static final ConfigProperty<String> VORTEX_MAX_FILE_SIZE = 
ConfigProperty
+      .key("hoodie.vortex.max.file.size")
+      .defaultValue(String.valueOf(120 * 1024 * 1024))
+      .markAdvanced()
+      .withDocumentation("Target file size in bytes for Vortex base files.");
+
+  public static final ConfigProperty<String> VORTEX_WRITE_ALLOCATOR_SIZE_BYTES 
= ConfigProperty
+      .key("hoodie.vortex.write.allocator.size.bytes")
+      .defaultValue(String.valueOf(256 * 1024 * 1024))
+      .markAdvanced()
+      .withDocumentation("Maximum size in bytes of the Arrow child allocator 
used by the Vortex "
+          + "writer for buffering in-flight batch data. Must be large enough 
that the Arrow "
+          + "buffer's power-of-2 doubling growth never requests a buffer 
exceeding this cap, "
+          + "otherwise writes fail with OutOfMemoryException.");
+
+  public static final ConfigProperty<String> VORTEX_WRITE_FLUSH_BYTE_WATERMARK 
= ConfigProperty
+      .key("hoodie.vortex.write.flush.byte.watermark")
+      .defaultValue(String.valueOf(96 * 1024 * 1024))
+      .markAdvanced()
+      .withDocumentation("Byte-size watermark on the Vortex writer's in-flight 
Arrow buffers; "
+          + "the writer flushes the current batch when the sum of FieldVector 
buffer sizes "
+          + "reaches this value, in addition to the row-count batch 
threshold.");
+
+  public static final ConfigProperty<String> VORTEX_READ_ALLOCATOR_SIZE_BYTES 
= ConfigProperty
+      .key("hoodie.vortex.read.allocator.size.bytes")
+      .defaultValue(String.valueOf(256 * 1024 * 1024))
+      .markAdvanced()
+      .withDocumentation("Maximum size in bytes of the Arrow child allocator 
used by the Vortex "
+          + "reader for per-read data buffers.");
+
+  public static final ConfigProperty<String> 
VORTEX_READ_METADATA_ALLOCATOR_SIZE_BYTES = ConfigProperty
+      .key("hoodie.vortex.read.metadata.allocator.size.bytes")
+      .defaultValue(String.valueOf(8 * 1024 * 1024))
+      .markAdvanced()
+      .withDocumentation("Maximum size in bytes of the Arrow child allocator 
used by the Vortex "
+          + "reader for footer/metadata operations (schema, bloom filter).");
+
   public static final ConfigProperty<Boolean> HFILE_WRITER_TO_ALLOW_DUPLICATES 
= ConfigProperty
       .key("hoodie.hfile.writes.allow.duplicates")
       .defaultValue(false)
@@ -632,6 +669,11 @@ public class HoodieStorageConfig extends HoodieConfig {
       return this;
     }
 
+    public Builder vortexMaxFileSize(long maxFileSize) {
+      storageConfig.setValue(VORTEX_MAX_FILE_SIZE, 
String.valueOf(maxFileSize));
+      return this;
+    }
+
     public Builder orcStripeSize(int orcStripeSize) {
       storageConfig.setValue(ORC_STRIPE_SIZE, String.valueOf(orcStripeSize));
       return this;
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/model/HoodieFileFormat.java 
b/hudi-common/src/main/java/org/apache/hudi/common/model/HoodieFileFormat.java
index 8410ad486dea..8961689ec9c8 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/model/HoodieFileFormat.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/model/HoodieFileFormat.java
@@ -53,12 +53,20 @@ public enum HoodieFileFormat {
 
   @EnumFieldDescription("Lance is a modern columnar data format optimized for 
random access patterns "
           + "and designed for ML and AI workloads")
-  LANCE(".lance");
+  LANCE(".lance"),
+
+  @EnumFieldDescription("Vortex is a next-generation columnar data format with 
cascading compression "
+          + "optimized for fast random access and analytical scans")
+  VORTEX(".vortex");
 
   public static final String LANCE_UNSUPPORTED_ERROR_MSG =
       "Lance base file format is not supported by this reader or writer path. "
           + "Please use an engine-specific Lance reader/writer, or use 
Parquet, ORC, or HFile.";
 
+  public static final String VORTEX_SPARK_ONLY_ERROR_MSG =
+      "Vortex base file format is currently only supported with the Spark 
engine. "
+          + "Please use Parquet, ORC, or HFile for non-Spark engines (Flink, 
Hive, Presto, Trino).";
+
   public static final Set<String> BASE_FILE_EXTENSIONS = 
Arrays.stream(HoodieFileFormat.values())
       .map(HoodieFileFormat::getFileExtension)
       .filter(x -> !x.equals(HoodieFileFormat.HOODIE_LOG.getFileExtension()))
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/util/VortexUtils.java 
b/hudi-common/src/main/java/org/apache/hudi/common/util/VortexUtils.java
new file mode 100644
index 000000000000..e51ab8910a4c
--- /dev/null
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/VortexUtils.java
@@ -0,0 +1,203 @@
+/*
+ * 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.hudi.common.util;
+
+import org.apache.hudi.common.config.HoodieReaderConfig;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieKey;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.CloseableMappingIterator;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.core.io.storage.HoodieFileReader;
+import org.apache.hudi.core.io.storage.HoodieIOFactory;
+import org.apache.hudi.exception.HoodieIOException;
+import org.apache.hudi.keygen.BaseKeyGenerator;
+import org.apache.hudi.metadata.HoodieIndexVersion;
+import org.apache.hudi.metadata.stats.HoodieColumnRangeMetadata;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.avro.generic.GenericRecord;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class VortexUtils extends FileFormatUtils {
+
+  @Override
+  public ClosableIterator<Pair<HoodieKey, Long>> 
fetchRecordKeysWithPositions(HoodieStorage storage, StoragePath filePath) {
+    return fetchRecordKeysWithPositions(storage, filePath, Option.empty(), 
Option.empty());
+  }
+
+  @Override
+  public ClosableIterator<Pair<HoodieKey, Long>> 
fetchRecordKeysWithPositions(HoodieStorage storage,
+                                                                               
StoragePath filePath,
+                                                                               
Option<BaseKeyGenerator> keyGeneratorOpt,
+                                                                               
Option<String> partitionPath) {
+    AtomicLong position = new AtomicLong(0);
+    return new CloseableMappingIterator<>(
+        getHoodieKeyIterator(storage, filePath, keyGeneratorOpt, 
partitionPath),
+        key -> Pair.of(key, position.getAndIncrement()));
+  }
+
+  @Override
+  public ClosableIterator<HoodieKey> getHoodieKeyIterator(HoodieStorage 
storage, StoragePath filePath) {
+    return getHoodieKeyIterator(storage, filePath, Option.empty(), 
Option.empty());
+  }
+
+  @Override
+  public ClosableIterator<HoodieKey> getHoodieKeyIterator(HoodieStorage 
storage,
+                                                           StoragePath 
filePath,
+                                                           
Option<BaseKeyGenerator> keyGeneratorOpt,
+                                                           Option<String> 
partitionPath) {
+    try {
+      HoodieSchema keySchema = getKeyIteratorSchema(storage, filePath, 
keyGeneratorOpt, partitionPath);
+      HoodieFileReader reader = HoodieIOFactory.getIOFactory(storage)
+          .getReaderFactory(HoodieRecord.HoodieRecordType.SPARK)
+          .getFileReader(new HoodieReaderConfig(), filePath, 
HoodieFileFormat.VORTEX);
+      ClosableIterator<HoodieRecord> recordIterator = 
reader.getRecordIterator(keySchema);
+
+      return new CloseableMappingIterator<>(
+          recordIterator,
+          record -> {
+            String recordKey;
+            if (keyGeneratorOpt.isPresent()) {
+              recordKey = record.getRecordKey(keySchema, keyGeneratorOpt);
+            } else {
+              recordKey = record.getRecordKey(keySchema, 
HoodieRecord.RECORD_KEY_METADATA_FIELD);
+            }
+            String partitionPathValue = partitionPath.orElseGet(() ->
+                (String) record.getColumnValueAsJava(keySchema, 
HoodieRecord.PARTITION_PATH_METADATA_FIELD,
+                    CollectionUtils.emptyProps()));
+            return new HoodieKey(recordKey, partitionPathValue);
+          }
+      );
+    } catch (IOException e) {
+      throw new HoodieIOException("Failed to read from Vortex file " + 
filePath, e);
+    }
+  }
+
+  @Override
+  public HoodieSchema readSchema(HoodieStorage storage, StoragePath filePath) {
+    try (HoodieFileReader fileReader =
+                 HoodieIOFactory.getIOFactory(storage)
+                         .getReaderFactory(HoodieRecord.HoodieRecordType.SPARK)
+                         .getFileReader(
+                                 ConfigUtils.DEFAULT_HUDI_CONFIG_FOR_READER,
+                                 filePath,
+                                 HoodieFileFormat.VORTEX)) {
+      return fileReader.getSchema();
+    } catch (IOException e) {
+      throw new HoodieIOException("Failed to read schema from Vortex file", e);
+    }
+  }
+
+  @Override
+  public HoodieFileFormat getFormat() {
+    return HoodieFileFormat.VORTEX;
+  }
+
+  @Override
+  public List<GenericRecord> readAvroRecords(HoodieStorage storage, 
StoragePath filePath) {
+    throw new UnsupportedOperationException("readAvroRecords is not yet 
supported for Vortex format");
+  }
+
+  @Override
+  public List<GenericRecord> readAvroRecords(HoodieStorage storage, 
StoragePath filePath, HoodieSchema schema) {
+    throw new UnsupportedOperationException("readAvroRecords with schema is 
not yet supported for Vortex format");
+  }
+
+  @Override
+  public Map<String, String> readFooter(HoodieStorage storage, boolean 
required, StoragePath filePath, String... footerNames) {
+    throw new UnsupportedOperationException("readFooter is not yet supported 
for Vortex format");
+  }
+
+  @Override
+  public long getRowCount(HoodieStorage storage, StoragePath filePath) {
+    try (HoodieFileReader fileReader =
+                 HoodieIOFactory.getIOFactory(storage)
+                         .getReaderFactory(HoodieRecord.HoodieRecordType.SPARK)
+                         .getFileReader(
+                                 ConfigUtils.DEFAULT_HUDI_CONFIG_FOR_READER,
+                                 filePath,
+                                 HoodieFileFormat.VORTEX)) {
+      return fileReader.getTotalRecords();
+    } catch (IOException e) {
+      throw new HoodieIOException("Failed to get row count from Vortex file", 
e);
+    }
+  }
+
+  @Override
+  public Set<Pair<String, Long>> filterRowKeys(HoodieStorage storage, 
StoragePath filePath, Set<String> filterKeys) {
+    try (HoodieFileReader fileReader =
+                 HoodieIOFactory.getIOFactory(storage)
+                         .getReaderFactory(HoodieRecord.HoodieRecordType.SPARK)
+                         .getFileReader(
+                                 ConfigUtils.DEFAULT_HUDI_CONFIG_FOR_READER,
+                                 filePath,
+                                 HoodieFileFormat.VORTEX)) {
+      return fileReader.filterRowKeys(filterKeys);
+    } catch (IOException e) {
+      throw new HoodieIOException("Failed to read filter keys from Vortex 
file", e);
+    }
+  }
+
+  @Override
+  public List<HoodieColumnRangeMetadata<Comparable>> 
readColumnStatsFromMetadata(HoodieStorage storage,
+                                                                               
   StoragePath filePath,
+                                                                               
   List<String> columnList,
+                                                                               
   HoodieIndexVersion indexVersion) {
+    throw new UnsupportedOperationException("readColumnStatsFromMetadata is 
not yet supported for Vortex format");
+  }
+
+  @Override
+  public void writeMetaFile(HoodieStorage storage, StoragePath filePath, 
Properties props) throws IOException {
+    throw new UnsupportedOperationException("writeMetaFile is not yet 
supported for Vortex format");
+  }
+
+  @Override
+  public ByteArrayOutputStream serializeRecordsToLogBlock(HoodieStorage 
storage,
+                                                          List<HoodieRecord> 
records,
+                                                          HoodieSchema 
writerSchema,
+                                                          HoodieSchema 
readerSchema,
+                                                          String keyFieldName,
+                                                          Map<String, String> 
paramsMap) throws IOException {
+    throw new UnsupportedOperationException("serializeRecordsToLogBlock is not 
yet supported for Vortex format");
+  }
+
+  @Override
+  public Pair<ByteArrayOutputStream, Object> 
serializeRecordsToLogBlock(HoodieStorage storage,
+                                                                        
Iterator<HoodieRecord> records,
+                                                                        
HoodieRecord.HoodieRecordType recordType,
+                                                                        
HoodieSchema writerSchema,
+                                                                        
HoodieSchema readerSchema,
+                                                                        String 
keyFieldName,
+                                                                        
Map<String, String> paramsMap) throws IOException {
+    throw new UnsupportedOperationException("serializeRecordsToLogBlock with 
iterator is not yet supported for Vortex format");
+  }
+}
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileReaderFactory.java
 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileReaderFactory.java
index 1be9b4f34282..ba0481171ae7 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileReaderFactory.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileReaderFactory.java
@@ -33,6 +33,7 @@ import static 
org.apache.hudi.common.model.HoodieFileFormat.HFILE;
 import static org.apache.hudi.common.model.HoodieFileFormat.LANCE;
 import static org.apache.hudi.common.model.HoodieFileFormat.ORC;
 import static org.apache.hudi.common.model.HoodieFileFormat.PARQUET;
+import static org.apache.hudi.common.model.HoodieFileFormat.VORTEX;
 
 /**
  * Factory methods to create Hudi file reader.
@@ -58,6 +59,9 @@ public class HoodieFileReaderFactory {
     if (LANCE.getFileExtension().equals(extension)) {
       return getFileReader(hoodieConfig, path, LANCE, Option.empty());
     }
+    if (VORTEX.getFileExtension().equals(extension)) {
+      return getFileReader(hoodieConfig, path, VORTEX, Option.empty());
+    }
     throw new UnsupportedOperationException(extension + " format not supported 
yet.");
   }
 
@@ -77,6 +81,8 @@ public class HoodieFileReaderFactory {
         return newOrcFileReader(path);
       case LANCE:
         return newLanceFileReader(hoodieConfig, path);
+      case VORTEX:
+        return newVortexFileReader(hoodieConfig, path);
       default:
         throw new UnsupportedOperationException(format + " format not 
supported yet.");
     }
@@ -137,6 +143,10 @@ public class HoodieFileReaderFactory {
     throw new 
UnsupportedOperationException(HoodieFileFormat.LANCE_UNSUPPORTED_ERROR_MSG);
   }
 
+  protected HoodieFileReader newVortexFileReader(HoodieConfig hoodieConfig, 
StoragePath path) {
+    throw new 
UnsupportedOperationException(HoodieFileFormat.VORTEX_SPARK_ONLY_ERROR_MSG);
+  }
+
   public HoodieFileReader newBootstrapFileReader(HoodieFileReader 
skeletonFileReader,
                                                  HoodieFileReader 
dataFileReader,
                                                  Option<String[]> 
partitionFields,
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileWriterFactory.java
 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileWriterFactory.java
index 7c8499e42c3d..2ac944085986 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileWriterFactory.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieFileWriterFactory.java
@@ -37,6 +37,7 @@ import static 
org.apache.hudi.common.model.HoodieFileFormat.HFILE;
 import static org.apache.hudi.common.model.HoodieFileFormat.LANCE;
 import static org.apache.hudi.common.model.HoodieFileFormat.ORC;
 import static org.apache.hudi.common.model.HoodieFileFormat.PARQUET;
+import static org.apache.hudi.common.model.HoodieFileFormat.VORTEX;
 
 public class HoodieFileWriterFactory {
   protected final HoodieStorage storage;
@@ -75,6 +76,9 @@ public class HoodieFileWriterFactory {
     if (LANCE.getFileExtension().equals(extension)) {
       return newLanceFileWriter(instantTime, path, config, schema, 
taskContextSupplier);
     }
+    if (VORTEX.getFileExtension().equals(extension)) {
+      return newVortexFileWriter(instantTime, path, config, schema, 
taskContextSupplier);
+    }
     throw new UnsupportedOperationException(extension + " format not supported 
yet.");
   }
 
@@ -117,6 +121,12 @@ public class HoodieFileWriterFactory {
     throw new 
UnsupportedOperationException(HoodieFileFormat.LANCE_UNSUPPORTED_ERROR_MSG);
   }
 
+  protected HoodieFileWriter newVortexFileWriter(
+      String instantTime, StoragePath path, HoodieConfig config, HoodieSchema 
schema,
+      TaskContextSupplier taskContextSupplier) throws IOException {
+    throw new 
UnsupportedOperationException(HoodieFileFormat.VORTEX_SPARK_ONLY_ERROR_MSG);
+  }
+
   public static BloomFilter createBloomFilter(HoodieConfig config) {
     return BloomFilterFactory.createBloomFilter(
         
config.getIntOrDefault(HoodieStorageConfig.BLOOM_FILTER_NUM_ENTRIES_VALUE),
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieIOFactory.java
 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieIOFactory.java
index 57dcc753c9d0..8e52f781c5b4 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieIOFactory.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/core/io/storage/HoodieIOFactory.java
@@ -114,6 +114,8 @@ public abstract class HoodieIOFactory {
       return getFileFormatUtils(HoodieFileFormat.HFILE);
     } else if 
(path.getFileExtension().equals(HoodieFileFormat.LANCE.getFileExtension())) {
       return getFileFormatUtils(HoodieFileFormat.LANCE);
+    } else if 
(path.getFileExtension().equals(HoodieFileFormat.VORTEX.getFileExtension())) {
+      return getFileFormatUtils(HoodieFileFormat.VORTEX);
     }
     throw new UnsupportedOperationException("The format for file " + path + " 
is not supported yet.");
   }
diff --git a/hudi-hadoop-common/pom.xml b/hudi-hadoop-common/pom.xml
index 7e6b8615fe1c..c0b106e982ff 100644
--- a/hudi-hadoop-common/pom.xml
+++ b/hudi-hadoop-common/pom.xml
@@ -164,4 +164,48 @@
       <artifactId>lance-core</artifactId>
     </dependency>
   </dependencies>
+
+  <profiles>
+    <!-- Vortex requires Java 17: dev.vortex:vortex-jni ships Java 17 bytecode 
(class file major
+         version 61), which javac 11 (used by the Java 11 CI jobs) cannot 
read. Compile the Vortex
+         sources under src/main/java-vortex and pull in vortex-jni only when 
building on JDK 17;
+         Java 8/11 builds skip the format entirely. -->
+    <profile>
+      <id>vortex</id>
+      <activation>
+        <jdk>[17,)</jdk>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>dev.vortex</groupId>
+          <artifactId>vortex-jni</artifactId>
+        </dependency>
+      </dependencies>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-vortex-source</id>
+                <phase>generate-sources</phase>
+                <goals>
+                  <goal>add-source</goal>
+                </goals>
+                <configuration>
+                  <!-- Vortex is only supported on Spark 3.5+/4.x; skip the 
source on Spark 3.3/3.4
+                       (vortex.skip.tests=true) where its Spark 3.5+ APIs are 
unavailable. -->
+                  <skipAddSource>${vortex.skip.tests}</skipAddSource>
+                  <sources>
+                    <source>src/main/java-vortex</source>
+                  </sources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git 
a/hudi-hadoop-common/src/main/java-vortex/org/apache/hudi/io/vortex/HoodieBaseVortexWriter.java
 
b/hudi-hadoop-common/src/main/java-vortex/org/apache/hudi/io/vortex/HoodieBaseVortexWriter.java
new file mode 100644
index 000000000000..c73d629152ec
--- /dev/null
+++ 
b/hudi-hadoop-common/src/main/java-vortex/org/apache/hudi/io/vortex/HoodieBaseVortexWriter.java
@@ -0,0 +1,345 @@
+/*
+ * 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.hudi.io.vortex;
+
+import org.apache.hudi.common.avro.HoodieBloomFilterWriteSupport;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.io.memory.HoodieArrowAllocator;
+import org.apache.hudi.storage.StoragePath;
+
+import dev.vortex.api.Session;
+import dev.vortex.api.VortexWriter;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.arrow.c.ArrowArray;
+import org.apache.arrow.c.ArrowSchema;
+import org.apache.arrow.c.Data;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.pojo.Schema;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+
+/**
+ * Base class for Hudi Vortex file writers supporting different record types.
+ *
+ * This class handles common Vortex file operations including:
+ * - {@link VortexWriter} lifecycle management (vortex-jni {@code 
dev.vortex.api})
+ * - BufferAllocator management
+ * - Record buffering and batch flushing. Each flushed batch is exported 
through the Arrow C Data
+ *   Interface and handed to {@link VortexWriter#writeBatch(long, long)}; 
because vortex copies the
+ *   batch without invoking the C release callback, {@link 
ArrowArray#release()} is called after each
+ *   write so the exported buffers are freed.
+ * - File size checks
+ *
+ * Subclasses must implement type-specific conversion to Arrow format and 
provide the Arrow schema.
+ *
+ * <p>NOTE: vortex-jni 0.76.0 exposes no file-level custom-metadata API, so 
Hudi's bloom filter and
+ * min/max record-key footers cannot be persisted into the Vortex file yet. 
Record keys are still
+ * tracked via {@link #bloomFilterWriteSupportOpt} but the finalized metadata 
is not written.
+ * TODO(#18623): persist bloom/min-max footers once Vortex supports file 
metadata.
+ *
+ * @param <R> The record type (e.g., GenericRecord, InternalRow)
+ */
+@Slf4j
+@NotThreadSafe
+public abstract class HoodieBaseVortexWriter<R, K extends Comparable<K>> 
implements Closeable {
+  protected static final int DEFAULT_BATCH_SIZE = 1000;
+  private final StoragePath path;
+  private final BufferAllocator allocator;
+  private final int batchSize;
+  private final long flushByteWatermark;
+  @Getter(value = AccessLevel.PROTECTED)
+  private long writtenRecordCount = 0;
+  private long totalFlushedDataSize = 0;
+  private int currentBatchSize = 0;
+  private VectorSchemaRoot root;
+  private ArrowWriter<R> arrowWriter;
+  protected final Option<HoodieBloomFilterWriteSupport<K>> 
bloomFilterWriteSupportOpt;
+
+  // Held for the writer's lifetime. session/nativeAllocator back the native 
writer; nativeAllocator
+  // is a dedicated allocator isolated from the strict data allocator (see 
initializeWriter).
+  private Session session;
+  private VortexWriter writer;
+  private BufferAllocator nativeAllocator;
+  private ArrowSchema exportedSchema;
+  private long exportedSchemaAddr;
+
+  /**
+   * Constructor for base Vortex writer.
+   *
+   * @param path Path where Vortex file will be written
+   * @param batchSize Row-count threshold; the current batch is flushed when 
this many records have been buffered
+   * @param allocatorSize Maximum bytes the per-writer Arrow child allocator 
may hold at once
+   * @param flushByteWatermark Byte-size threshold; the current batch is 
flushed when the sum of in-flight
+   *                           FieldVector buffer sizes reaches this value
+   * @param bloomFilterWriteSupportOpt Optional bloom filter write support for 
record key tracking
+   */
+  protected HoodieBaseVortexWriter(StoragePath path, int batchSize, long 
allocatorSize, long flushByteWatermark,
+                                   Option<HoodieBloomFilterWriteSupport<K>> 
bloomFilterWriteSupportOpt) {
+    this.path = path;
+    this.allocator = HoodieArrowAllocator.newChildAllocator(
+        getClass().getSimpleName() + "-data-" + path.getName(), allocatorSize);
+    this.batchSize = batchSize;
+    this.flushByteWatermark = flushByteWatermark;
+    this.bloomFilterWriteSupportOpt = bloomFilterWriteSupportOpt;
+  }
+
+  /**
+   * Create and initialize the arrow writer for writing records to 
VectorSchemaRoot.
+   * Called once during lazy initialization when the first record is written.
+   *
+   * @param root The VectorSchemaRoot to write into
+   * @return An arrow writer implementation that writes records of type R to 
the root
+   */
+  protected abstract ArrowWriter<R> createArrowWriter(VectorSchemaRoot root);
+
+  /**
+   * Get the Arrow schema for this writer. It is passed directly to {@link 
VortexWriter} and each
+   * written batch must conform to it.
+   *
+   * @return Arrow schema
+   */
+  protected abstract Schema getArrowSchema();
+
+  /**
+   * Write a single record. Records are buffered and flushed in batches.
+   *
+   * @param record Record to write
+   * @throws IOException if write fails
+   */
+  public void write(R record) throws IOException {
+    // Lazy initialization on first write
+    if (writer == null) {
+      initializeWriter();
+    }
+    if (root == null) {
+      root = VectorSchemaRoot.create(getArrowSchema(), allocator);
+    }
+    if (arrowWriter == null) {
+      arrowWriter = createArrowWriter(root);
+    }
+
+    // Reset arrow writer at the start of each new batch
+    if (currentBatchSize == 0) {
+      arrowWriter.reset();
+    }
+
+    arrowWriter.write(record);
+    currentBatchSize++;
+    writtenRecordCount++;
+
+    // Flush when row-count batch is full OR in-flight Arrow buffers cross the 
byte watermark.
+    if (currentBatchSize >= batchSize || currentBufferBytes() >= 
flushByteWatermark) {
+      flushBatch();
+    }
+  }
+
+  /**
+   * Bytes currently held by the writer's Arrow child allocator.
+   */
+  private long currentBufferBytes() {
+    return allocator.getAllocatedMemory();
+  }
+
+  /**
+   * Close the writer, flushing any remaining buffered records.
+   *
+   * @throws IOException if close fails
+   */
+  @Override
+  public void close() throws IOException {
+    Exception primaryException = null;
+
+    // 1. Flush remaining records
+    try {
+      if (currentBatchSize > 0) {
+        flushBatch();
+      }
+
+      // Ensure the file is created even if no data was written, by emitting a 
single empty batch.
+      if (writer == null) {
+        initializeWriter();
+        try (VectorSchemaRoot emptyRoot = 
VectorSchemaRoot.create(getArrowSchema(), allocator)) {
+          emptyRoot.setRowCount(0);
+          writeBatchFfi(emptyRoot);
+        }
+      }
+    } catch (Exception e) {
+      primaryException = e;
+    }
+
+    // Close Vortex writer (finalizes the file)
+    if (writer != null) {
+      try {
+        writer.close();
+      } catch (Exception e) {
+        primaryException = addSuppressed(primaryException, e);
+      }
+    }
+
+    // Close the exported schema handle.
+    if (exportedSchema != null) {
+      try {
+        exportedSchema.release();
+        exportedSchema.close();
+      } catch (Exception e) {
+        primaryException = addSuppressed(primaryException, e);
+      }
+    }
+
+    // Close VectorSchemaRoot
+    if (root != null) {
+      try {
+        root.close();
+      } catch (Exception e) {
+        primaryException = addSuppressed(primaryException, e);
+      }
+    }
+
+    // Close the strict data allocator (per-batch exports are released, so 
this should be clean).
+    try {
+      allocator.close();
+    } catch (Exception e) {
+      primaryException = addSuppressed(primaryException, e);
+    }
+
+    // Close the native allocator last. vortex-jni 0.76.0's 
VortexWriter.create exports the file
+    // schema into this allocator and relies on GC/Cleaner-based cleanup 
rather than freeing it on
+    // close(), so a small one-time amount can still be outstanding here; 
tolerate it (it is freed
+    // when the native writer is reclaimed) rather than failing the whole 
write.
+    // TODO(#18623): drop this once vortex-jni frees the create() schema 
export on writer close.
+    if (nativeAllocator != null) {
+      try {
+        nativeAllocator.close();
+      } catch (IllegalStateException e) {
+        log.warn("Vortex native allocator retained memory at close for {} 
(vortex-jni create() "
+            + "schema export, reclaimed on GC): {}", path, e.getMessage());
+      }
+    }
+
+    if (primaryException != null) {
+      throw new HoodieException("Failed to close Vortex writer: " + path, 
primaryException);
+    }
+  }
+
+  private static Exception addSuppressed(Exception primary, Exception e) {
+    if (primary == null) {
+      return e;
+    }
+    primary.addSuppressed(e);
+    return primary;
+  }
+
+  /**
+   * Returns the estimated data size in bytes, including both flushed batches 
and the current
+   * in-progress batch.
+   */
+  protected long getDataSize() {
+    long currentBufferSize = currentBatchSize > 0 ? 
allocator.getAllocatedMemory() : 0;
+    return totalFlushedDataSize + currentBufferSize;
+  }
+
+  /**
+   * Flush buffered records to Vortex file.
+   */
+  private void flushBatch() throws IOException {
+    if (currentBatchSize == 0) {
+      return;
+    }
+
+    arrowWriter.finishBatch();
+
+    for (FieldVector vector : root.getFieldVectors()) {
+      totalFlushedDataSize += vector.getBufferSize();
+    }
+
+    writeBatchFfi(root);
+
+    // Release Arrow buffers so capacity does not accumulate across batches.
+    root.close();
+    root = null;
+    arrowWriter = null;
+
+    currentBatchSize = 0;
+  }
+
+  /**
+   * Export a VectorSchemaRoot's array through the Arrow C Data Interface and 
write it to the Vortex
+   * file. The array is exported from the strict data allocator and released 
after the write (vortex
+   * copies the batch but does not invoke the C release callback), so no 
data-allocator memory leaks.
+   */
+  private void writeBatchFfi(VectorSchemaRoot batch) throws IOException {
+    try (ArrowArray cArray = ArrowArray.allocateNew(allocator)) {
+      Data.exportVectorSchemaRoot(allocator, batch, null, cArray);
+      writer.writeBatch(cArray.memoryAddress(), exportedSchemaAddr);
+      cArray.release();
+    }
+  }
+
+  /**
+   * Initialize the {@link VortexWriter} (lazy initialization). The writer and 
its exported schema
+   * use a dedicated {@link #nativeAllocator} so the native side's 
non-deterministic cleanup does
+   * not pollute the strict per-batch data allocator.
+   */
+  private void initializeWriter() throws IOException {
+    session = Session.create();
+    nativeAllocator = new RootAllocator();
+    Schema arrowSchema = getArrowSchema();
+    writer = VortexWriter.create(session, toVortexUri(path), arrowSchema, 
Collections.emptyMap(), nativeAllocator);
+    exportedSchema = ArrowSchema.allocateNew(nativeAllocator);
+    Data.exportSchema(nativeAllocator, arrowSchema, null, exportedSchema);
+    exportedSchemaAddr = exportedSchema.memoryAddress();
+  }
+
+  /**
+   * Vortex requires a well-formed URL. Hudi {@link StoragePath}s for remote 
stores already carry a
+   * scheme (s3://, hdfs://, ...); local paths may not, so qualify them as 
{@code file://} URIs.
+   */
+  private static String toVortexUri(StoragePath path) {
+    URI uri = path.toUri();
+    if (uri.getScheme() == null) {
+      return new File(uri.getPath()).toURI().toString();
+    }
+    return uri.toString();
+  }
+
+  /**
+   * Arrow writer interface for writing records of type T to a 
VectorSchemaRoot.
+   * @param <T> the record type
+   */
+  protected interface ArrowWriter<T> {
+    void write(T row) throws IOException;
+
+    void finishBatch() throws IOException;
+
+    void reset();
+  }
+}
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieHadoopIOFactory.java
 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieHadoopIOFactory.java
index 0dcc6dacb823..550203002354 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieHadoopIOFactory.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieHadoopIOFactory.java
@@ -28,6 +28,7 @@ import org.apache.hudi.common.util.LanceUtils;
 import org.apache.hudi.common.util.OrcUtils;
 import org.apache.hudi.common.util.ParquetUtils;
 import org.apache.hudi.common.util.ReflectionUtils;
+import org.apache.hudi.common.util.VortexUtils;
 import org.apache.hudi.core.io.storage.HoodieFileReaderFactory;
 import org.apache.hudi.core.io.storage.HoodieFileWriterFactory;
 import org.apache.hudi.core.io.storage.HoodieIOFactory;
@@ -114,6 +115,8 @@ public class HoodieHadoopIOFactory extends HoodieIOFactory {
         return new HFileUtils();
       case LANCE:
         return new LanceUtils();
+      case VORTEX:
+        return new VortexUtils();
       default:
         throw new UnsupportedOperationException(fileFormat.name() + " format 
not supported yet.");
     }
diff --git 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HiveHoodieReaderContext.java
 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HiveHoodieReaderContext.java
index ad887526441d..93723e99a9ea 100644
--- 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HiveHoodieReaderContext.java
+++ 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HiveHoodieReaderContext.java
@@ -132,6 +132,9 @@ public class HiveHoodieReaderContext extends 
HoodieReaderContext<ArrayWritable>
     if 
(filePath.toString().endsWith(HoodieFileFormat.LANCE.getFileExtension())) {
       throw new 
UnsupportedOperationException(HoodieFileFormat.LANCE_UNSUPPORTED_ERROR_MSG);
     }
+    if 
(filePath.toString().endsWith(HoodieFileFormat.VORTEX.getFileExtension())) {
+      throw new 
UnsupportedOperationException(HoodieFileFormat.VORTEX_SPARK_ONLY_ERROR_MSG);
+    }
     // mdt file schema irregular and does not work with this logic. Also, log 
file evolution is handled inside the log block
     boolean isParquetOrOrc = 
filePath.getFileExtension().equals(HoodieFileFormat.PARQUET.getFileExtension())
         || 
filePath.getFileExtension().equals(HoodieFileFormat.ORC.getFileExtension());
diff --git 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieVortexInputFormat.java
 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieVortexInputFormat.java
new file mode 100644
index 000000000000..6f59b9caf194
--- /dev/null
+++ 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieVortexInputFormat.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hudi.hadoop;
+
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.hadoop.utils.HoodieInputFormatUtils;
+
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.io.ArrayWritable;
+import org.apache.hadoop.io.NullWritable;
+import org.apache.hadoop.mapred.InputSplit;
+import org.apache.hadoop.mapred.JobConf;
+import org.apache.hadoop.mapred.RecordReader;
+import org.apache.hadoop.mapred.Reporter;
+
+import java.io.IOException;
+
+/**
+ * HoodieInputFormat for HUDI datasets which store data in Vortex base file 
format.
+ * <p>
+ * This class is required for catalog/metastore registration during CREATE 
TABLE operations.
+ * <p>
+ * TODO(#18623): Vortex reading through Hive InputFormat is not yet supported. 
When support is
+ * added, this should route through {@link 
HoodieFileGroupReaderBasedRecordReader} (like
+ * {@link HoodieParquetInputFormat} does) instead of a standalone record 
reader,
+ * to get MOR log merging, schema evolution, and bootstrap support for free.
+ *
+ * @see <a href="https://github.com/apache/hudi/issues/18623";>#18623</a>
+ */
+@UseFileSplitsFromInputFormat
+public class HoodieVortexInputFormat extends HoodieCopyOnWriteTableInputFormat 
{
+
+  protected HoodieTimeline filterInstantsTimeline(HoodieTimeline timeline) {
+    return HoodieInputFormatUtils.filterInstantsTimeline(timeline);
+  }
+
+  @Override
+  public RecordReader<NullWritable, ArrayWritable> getRecordReader(final 
InputSplit split, final JobConf job,
+      final Reporter reporter) throws IOException {
+    throw new UnsupportedOperationException(
+        "Vortex reading through Hive InputFormat is not yet supported. "
+            + "Use the Spark datasource path (spark.read.format(\"hudi\")) to 
read Vortex tables.");
+  }
+
+  @Override
+  protected boolean isSplitable(FileSystem fs, Path filename) {
+    // Vortex files are not splittable.
+    return false;
+  }
+}
diff --git 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/HoodieVortexRealtimeInputFormat.java
 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/HoodieVortexRealtimeInputFormat.java
new file mode 100644
index 000000000000..789f8a1eacc5
--- /dev/null
+++ 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/HoodieVortexRealtimeInputFormat.java
@@ -0,0 +1,64 @@
+/*
+ * 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.hudi.hadoop.realtime;
+
+import org.apache.hudi.hadoop.UseFileSplitsFromInputFormat;
+import org.apache.hudi.hadoop.UseRecordReaderFromInputFormat;
+
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.io.ArrayWritable;
+import org.apache.hadoop.io.NullWritable;
+import org.apache.hadoop.mapred.InputSplit;
+import org.apache.hadoop.mapred.JobConf;
+import org.apache.hadoop.mapred.RecordReader;
+import org.apache.hadoop.mapred.Reporter;
+
+import java.io.IOException;
+
+/**
+ * HoodieRealtimeInputFormat for HUDI datasets which store data in Vortex base 
file format.
+ * <p>
+ * This class is required for catalog/metastore registration during CREATE 
TABLE operations
+ * for MOR tables with Vortex base file format.
+ * <p>
+ * TODO(#18623): Vortex reading through Hive InputFormat is not yet supported. 
When support is
+ * added, this should route through {@code 
HoodieFileGroupReaderBasedRecordReader} to get
+ * unified MOR log merging, schema evolution, and bootstrap support.
+ *
+ * @see <a href="https://github.com/apache/hudi/issues/18623";>#18623</a>
+ */
+@UseRecordReaderFromInputFormat
+@UseFileSplitsFromInputFormat
+public class HoodieVortexRealtimeInputFormat extends 
HoodieMergeOnReadTableInputFormat {
+
+  @Override
+  public RecordReader<NullWritable, ArrayWritable> getRecordReader(final 
InputSplit split, final JobConf jobConf,
+      final Reporter reporter) throws IOException {
+    throw new UnsupportedOperationException(
+        "Vortex reading through Hive InputFormat is not yet supported. "
+            + "Use the Spark datasource path (spark.read.format(\"hudi\")) to 
read Vortex tables.");
+  }
+
+  @Override
+  protected boolean isSplitable(FileSystem fs, Path filename) {
+    // Vortex files are not splittable.
+    return false;
+  }
+}
diff --git 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/utils/HoodieInputFormatUtils.java
 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/utils/HoodieInputFormatUtils.java
index 1d00061ca94f..44f73d5a6b37 100644
--- 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/utils/HoodieInputFormatUtils.java
+++ 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/utils/HoodieInputFormatUtils.java
@@ -43,6 +43,7 @@ import org.apache.hudi.hadoop.FileStatusWithBootstrapBaseFile;
 import org.apache.hudi.hadoop.HoodieHFileInputFormat;
 import org.apache.hudi.hadoop.HoodieLanceInputFormat;
 import org.apache.hudi.hadoop.HoodieParquetInputFormat;
+import org.apache.hudi.hadoop.HoodieVortexInputFormat;
 import org.apache.hudi.hadoop.LocatedFileStatusWithBootstrapBaseFile;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.hadoop.realtime.HoodieHFileRealtimeInputFormat;
@@ -50,6 +51,7 @@ import 
org.apache.hudi.hadoop.realtime.HoodieLanceRealtimeInputFormat;
 import org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat;
 import org.apache.hudi.hadoop.realtime.HoodieRealtimeFileSplit;
 import org.apache.hudi.hadoop.realtime.HoodieRealtimePath;
+import org.apache.hudi.hadoop.realtime.HoodieVortexRealtimeInputFormat;
 import org.apache.hudi.hadoop.realtime.RealtimeSplit;
 import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StoragePath;
@@ -135,6 +137,16 @@ public class HoodieInputFormatUtils {
           inputFormat.setConf(conf);
           return inputFormat;
         }
+      case VORTEX:
+        if (realtime) {
+          HoodieVortexRealtimeInputFormat inputFormat = new 
HoodieVortexRealtimeInputFormat();
+          inputFormat.setConf(conf);
+          return inputFormat;
+        } else {
+          HoodieVortexInputFormat inputFormat = new HoodieVortexInputFormat();
+          inputFormat.setConf(conf);
+          return inputFormat;
+        }
       default:
         throw new HoodieIOException("Hoodie InputFormat not implemented for 
base file format " + baseFileFormat);
     }
@@ -164,6 +176,12 @@ public class HoodieInputFormatUtils {
         } else {
           return HoodieLanceInputFormat.class.getName();
         }
+      case VORTEX:
+        if (realtime) {
+          return HoodieVortexRealtimeInputFormat.class.getName();
+        } else {
+          return HoodieVortexInputFormat.class.getName();
+        }
       case ORC:
         return OrcInputFormat.class.getName();
       default:
@@ -176,6 +194,7 @@ public class HoodieInputFormatUtils {
       case PARQUET:
       case HFILE:
       case LANCE:
+      case VORTEX:
         return MapredParquetOutputFormat.class.getName();
       case ORC:
         return OrcOutputFormat.class.getName();
@@ -189,6 +208,7 @@ public class HoodieInputFormatUtils {
       case PARQUET:
       case HFILE:
       case LANCE:
+      case VORTEX:
         return ParquetHiveSerDe.class.getName();
       case ORC:
         return OrcSerde.class.getName();
@@ -208,6 +228,9 @@ public class HoodieInputFormatUtils {
     if (extension.equals(HoodieFileFormat.LANCE.getFileExtension())) {
       return getInputFormat(HoodieFileFormat.LANCE, realtime, conf);
     }
+    if (extension.equals(HoodieFileFormat.VORTEX.getFileExtension())) {
+      return getInputFormat(HoodieFileFormat.VORTEX, realtime, conf);
+    }
     // now we support read log file, try to find log file
     if (HadoopFSUtils.isLogFile(new Path(path)) && realtime) {
       return getInputFormat(HoodieFileFormat.PARQUET, realtime, conf);
diff --git a/hudi-spark-datasource/hudi-spark-common/pom.xml 
b/hudi-spark-datasource/hudi-spark-common/pom.xml
index 355373334c19..1dca8ee367a8 100644
--- a/hudi-spark-datasource/hudi-spark-common/pom.xml
+++ b/hudi-spark-datasource/hudi-spark-common/pom.xml
@@ -264,4 +264,48 @@
 
   </dependencies>
 
+  <profiles>
+    <!-- Vortex requires Java 17 (vortex-jni/vortex-spark ship Java 17 
bytecode). Compile the Vortex
+         datasource reader under src/main/scala-vortex and pull the 
vortex-spark connector only on
+         JDK 17; Java 11 builds skip it. Only the Spark 4.x adapters (Java 17) 
wire it in; the Spark
+         3.x adapters return None. -->
+    <profile>
+      <id>vortex</id>
+      <activation>
+        <jdk>[17,)</jdk>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>dev.vortex</groupId>
+          <artifactId>${vortex.spark.artifact}</artifactId>
+        </dependency>
+      </dependencies>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-vortex-source</id>
+                <phase>generate-sources</phase>
+                <goals>
+                  <goal>add-source</goal>
+                </goals>
+                <configuration>
+                  <!-- Vortex is only supported on Spark 3.5+/4.x; skip the 
source on Spark 3.3/3.4
+                       (vortex.skip.tests=true) where its Spark 3.5+ APIs are 
unavailable. -->
+                  <skipAddSource>${vortex.skip.tests}</skipAddSource>
+                  <sources>
+                    <source>src/main/scala-vortex</source>
+                  </sources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
 </project>
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala-vortex/org/apache/spark/sql/execution/datasources/vortex/SparkVortexReaderBase.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala-vortex/org/apache/spark/sql/execution/datasources/vortex/SparkVortexReaderBase.scala
new file mode 100644
index 000000000000..946638d41f49
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala-vortex/org/apache/spark/sql/execution/datasources/vortex/SparkVortexReaderBase.scala
@@ -0,0 +1,203 @@
+/*
+ * 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.spark.sql.execution.datasources.vortex
+
+import org.apache.hudi.SparkAdapterSupport.sparkAdapter
+import org.apache.hudi.common.config.HoodieStorageConfig
+import org.apache.hudi.common.schema.internal.InternalSchema
+import org.apache.hudi.common.util
+import org.apache.hudi.common.util.collection.ClosableIterator
+import org.apache.hudi.io.memory.HoodieArrowAllocator
+import org.apache.hudi.io.storage.VortexRecordIterator
+import org.apache.hudi.storage.StorageConfiguration
+
+import dev.vortex.api.{DataSource, ScanOptions, Session}
+import org.apache.hadoop.conf.Configuration
+import org.apache.parquet.schema.MessageType
+import org.apache.spark.TaskContext
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{AttributeReference, 
JoinedRow, UnsafeProjection, UnsafeRow}
+import 
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
+import org.apache.spark.sql.execution.datasources.{PartitionedFile, 
SparkColumnarFileReader, SparkSchemaTransformUtils}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.sources.Filter
+import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.util.ArrowUtils
+
+import java.io.IOException
+import java.net.URI
+
+import scala.collection.JavaConverters._
+
+/**
+ * Reader for Vortex files in the Spark datasource query path (vortex-jni 
0.76.0). Opens the file
+ * via `Session`/`DataSource`, scans it (reading all columns), and converts 
Arrow batches to Spark
+ * rows through [[VortexRecordIterator]], which projects the requested columns 
by name.
+ *
+ * Mirrors 
[[org.apache.spark.sql.execution.datasources.lance.SparkLanceReaderBase]] for 
the
+ * initial Vortex scope: standard Hudi data types, no filter pushdown/data 
skipping, and no
+ * BLOB/VECTOR handling.
+ *
+ * @param enableVectorizedReader whether to use vectorized reading (currently 
always true for Vortex)
+ */
+class SparkVortexReaderBase(enableVectorizedReader: Boolean) extends 
SparkColumnarFileReader {
+
+  /**
+   * Read a Vortex file with schema projection and partition column support.
+   *
+   * @param file              Vortex file to read
+   * @param requiredSchema    desired output schema of the data (columns to 
read)
+   * @param partitionSchema   schema of the partition columns. Partition 
values will be appended to the end of every row
+   * @param internalSchemaOpt option of internal schema for schema.on.read 
(not currently used for Vortex)
+   * @param filters           filters for data skipping. Not guaranteed to be 
used; the spark plan will also apply the filters.
+   * @param storageConf       the hadoop conf
+   * @param tableSchemaOpt    option of table schema for timestamp precision 
conversion fix.
+   * @return iterator of rows read from the file output type says 
[[InternalRow]] but could be [[org.apache.spark.sql.vectorized.ColumnarBatch]]
+   */
+  override def read(file: PartitionedFile,
+                    requiredSchema: StructType,
+                    partitionSchema: StructType,
+                    internalSchemaOpt: util.Option[InternalSchema],
+                    filters: scala.Seq[Filter],
+                    storageConf: StorageConfiguration[Configuration],
+                    tableSchemaOpt: util.Option[MessageType] = 
util.Option.empty()): Iterator[InternalRow] = {
+
+    val uri = 
toVortexUri(sparkAdapter.getSparkPartitionedFileUtils.getPathFromPartitionedFile(file).toUri)
+
+    if (requiredSchema.isEmpty && partitionSchema.isEmpty) {
+      // No columns requested - return empty iterator
+      Iterator.empty
+    } else {
+
+      val dataAllocatorSize = storageConf.unwrap().getLong(
+        HoodieStorageConfig.VORTEX_READ_ALLOCATOR_SIZE_BYTES.key(),
+        
HoodieStorageConfig.VORTEX_READ_ALLOCATOR_SIZE_BYTES.defaultValue().toLong)
+      val allocator = HoodieArrowAllocator.newChildAllocator(
+        getClass.getSimpleName + "-data-" + uri, dataAllocatorSize)
+
+      var vortexIterator: ClosableIterator[UnsafeRow] = null
+      var allocatorClosed = false
+
+      try {
+        // Session/DataSource are not AutoCloseable in vortex-jni 0.76.0 
(native cleanup is implicit).
+        val session = Session.create()
+        val dataSource = DataSource.open(session, uri)
+
+        // Convert the Vortex file schema to a Spark schema, then reconcile it 
against the requested
+        // schema for schema evolution (implicit type changes, columns not 
present in the file).
+        val fileSchema = 
ArrowUtils.fromArrowSchema(dataSource.arrowSchema(allocator))
+        val (implicitTypeChangeInfo, sparkRequestSchema) =
+          SparkSchemaTransformUtils.buildImplicitSchemaChangeInfo(fileSchema, 
requiredSchema)
+
+        // Only read columns that actually exist in the file; missing columns 
are null-padded below.
+        val requestSchema =
+          
SparkSchemaTransformUtils.filterSchemaByFileSchema(sparkRequestSchema, 
fileSchema)
+
+        val baseIter: Iterator[InternalRow] = if (requestSchema.isEmpty) {
+          // No data columns to read from the file (only partition columns 
requested, or all
+          // requested columns are new via schema evolution). Emit one empty 
row per record so the
+          // null-padding and partition projections can fill in the values.
+          val numRows = dataSource.rowCount().asOptional().orElse(0L)
+          allocator.close()
+          allocatorClosed = true
+          val emptyRow = InternalRow.empty
+          // Emit numRows empty rows without narrowing the long count to int 
(a >2^31-row file
+          // would otherwise overflow and silently yield zero rows).
+          new Iterator[InternalRow] {
+            private var remaining = numRows
+            override def hasNext: Boolean = remaining > 0
+            override def next(): InternalRow = {
+              remaining -= 1
+              emptyRow
+            }
+          }
+        } else {
+          val scan = dataSource.scan(ScanOptions.of())
+          vortexIterator = new VortexRecordIterator(allocator, session, 
dataSource, scan, requestSchema, uri)
+
+          // Register cleanup listener
+          Option(TaskContext.get()).foreach { ctx =>
+            ctx.addTaskCompletionListener[Unit](_ => vortexIterator.close())
+          }
+
+          vortexIterator.asScala
+        }
+
+        // Create the following projections for schema evolution:
+        // 1. Padding projection: add NULL for missing columns
+        // 2. Casting projection: handle implicit type conversions
+        val schemaUtils = sparkAdapter.getSchemaUtils
+        val paddingProj = 
SparkSchemaTransformUtils.generateNullPaddingProjection(requestSchema, 
requiredSchema)
+        val castProj = SparkSchemaTransformUtils.generateUnsafeProjection(
+          schemaUtils.toAttributes(requiredSchema),
+          Some(SQLConf.get.sessionLocalTimeZone),
+          implicitTypeChangeInfo,
+          requiredSchema,
+          new StructType(),
+          schemaUtils
+        )
+
+        // Unify projections by applying padding and then casting for each row
+        val projection: UnsafeProjection = new UnsafeProjection {
+          def apply(row: InternalRow): UnsafeRow =
+            castProj(paddingProj(row))
+        }
+        val projectedIter = baseIter.map(projection.apply)
+
+        // Handle partition columns
+        if (partitionSchema.length == 0) {
+          // No partition columns - return rows directly
+          projectedIter
+        } else {
+          // Create UnsafeProjection to convert JoinedRow to UnsafeRow
+          val fullSchema = (requiredSchema.fields ++ 
partitionSchema.fields).map(f =>
+            AttributeReference(f.name, f.dataType, f.nullable, f.metadata)())
+          val unsafeProjection = GenerateUnsafeProjection.generate(fullSchema, 
fullSchema)
+
+          // Append partition values to each row using JoinedRow, then convert 
to UnsafeRow
+          val joinedRow = new JoinedRow()
+          projectedIter.map(row => unsafeProjection(joinedRow(row, 
file.partitionValues)))
+        }
+      } catch {
+        case e: Exception =>
+          if (vortexIterator != null) {
+            vortexIterator.close() // Closes the iterator, which owns the 
allocator lifecycle
+          } else if (!allocatorClosed) {
+            allocator.close()
+          }
+          throw new IOException(s"Failed to read Vortex file: $uri", e)
+      }
+    }
+  }
+
+  /**
+   * Vortex requires a well-formed URL. Remote-store paths already carry a 
scheme
+   * (s3://, hdfs://, ...); local paths may not, so qualify them as `file://` 
URIs. Takes an
+   * already-parsed URI (from the storage path) rather than re-parsing a 
display string, so paths
+   * with spaces or other characters needing URI encoding are handled 
correctly.
+   */
+  private def toVortexUri(parsed: URI): String = {
+    if (parsed.getScheme == null) {
+      new java.io.File(parsed.getPath).toURI.toString
+    } else {
+      parsed.toString
+    }
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala
index e02931b778ef..bb321616a1eb 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala
@@ -117,7 +117,8 @@ class HoodieMergeOnReadRDDV2(@transient sc: SparkContext,
         val parquetReader = sparkAdapter.createParquetFileReader(vectorized = 
false, sqlConf, updatedOptions, config)
         val orcReader = sparkAdapter.createOrcFileReader(vectorized = false, 
sqlConf, updatedOptions, config, tableSchema.structTypeSchema)
         val lanceReader = sparkAdapter.createLanceFileReader(vectorized = 
false, sqlConf, updatedOptions, config).orNull
-        val multiReader = new MultipleColumnarFileFormatReader(parquetReader, 
orcReader, lanceReader)
+        val vortexReader = sparkAdapter.createVortexFileReader(vectorized = 
false, sqlConf, updatedOptions, config).orNull
+        val multiReader = new MultipleColumnarFileFormatReader(parquetReader, 
orcReader, lanceReader, vortexReader)
         sc.broadcast(multiReader)
       } else if (metaClient.getTableConfig.getBaseFileFormat == 
HoodieFileFormat.PARQUET) {
         sc.broadcast(sparkAdapter.createParquetFileReader(vectorized = false, 
sqlConf, updatedOptions, config))
@@ -125,6 +126,8 @@ class HoodieMergeOnReadRDDV2(@transient sc: SparkContext,
         sc.broadcast(sparkAdapter.createOrcFileReader(vectorized = false, 
sqlConf, updatedOptions, config, tableSchema.structTypeSchema))
       } else if (metaClient.getTableConfig.getBaseFileFormat == 
HoodieFileFormat.LANCE) {
         sc.broadcast(sparkAdapter.createLanceFileReader(vectorized = false, 
sqlConf, updatedOptions, config).orNull)
+      } else if (metaClient.getTableConfig.getBaseFileFormat == 
HoodieFileFormat.VORTEX) {
+        sc.broadcast(sparkAdapter.createVortexFileReader(vectorized = false, 
sqlConf, updatedOptions, config).orNull)
       } else {
         throw new IllegalArgumentException(s"Unsupported base file format: 
${metaClient.getTableConfig.getBaseFileFormat}")
       }
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
index 429b769d5b7a..4b4918ad2a7c 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala
@@ -363,13 +363,16 @@ class HoodieFileGroupReaderBasedFileFormat(tablePath: 
String,
       val parquetReader = 
sparkAdapter.createParquetFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration)
       val orcReader = sparkAdapter.createOrcFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration, dataSchema)
       val lanceReader = 
sparkAdapter.createLanceFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration).orNull
-      new MultipleColumnarFileFormatReader(parquetReader, orcReader, 
lanceReader)
+      val vortexReader = 
sparkAdapter.createVortexFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration).orNull
+      new MultipleColumnarFileFormatReader(parquetReader, orcReader, 
lanceReader, vortexReader)
     } else if (hoodieFileFormat == HoodieFileFormat.PARQUET) {
       sparkAdapter.createParquetFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration)
     } else if (hoodieFileFormat == HoodieFileFormat.ORC) {
       sparkAdapter.createOrcFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration, dataSchema)
     } else if (hoodieFileFormat == HoodieFileFormat.LANCE) {
       sparkAdapter.createLanceFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration).orNull
+    } else if (hoodieFileFormat == HoodieFileFormat.VORTEX) {
+      sparkAdapter.createVortexFileReader(enableVectorizedRead, 
spark.sessionState.conf, options, configuration).orNull
     } else {
       throw new HoodieNotSupportedException("Unsupported file format: " + 
hoodieFileFormat)
     }
diff --git a/hudi-spark-datasource/hudi-spark/pom.xml 
b/hudi-spark-datasource/hudi-spark/pom.xml
index efc0f3391523..bf7e2dbc5ce3 100644
--- a/hudi-spark-datasource/hudi-spark/pom.xml
+++ b/hudi-spark-datasource/hudi-spark/pom.xml
@@ -498,4 +498,47 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
+
+  <profiles>
+    <!-- Vortex requires Java 17 (vortex-jni/vortex-spark ship Java 17 
bytecode). The Vortex
+         round-trip test compiles/runs only under JDK 17; Java 11 builds skip 
it. -->
+    <profile>
+      <id>vortex</id>
+      <activation>
+        <jdk>[17,)</jdk>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>dev.vortex</groupId>
+          <artifactId>${vortex.spark.artifact}</artifactId>
+          <scope>test</scope>
+        </dependency>
+      </dependencies>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-vortex-test-source</id>
+                <phase>generate-test-sources</phase>
+                <goals>
+                  <goal>add-test-source</goal>
+                </goals>
+                <configuration>
+                  <!-- Vortex is only supported on Spark 3.5+/4.x; skip the 
test on Spark 3.3/3.4
+                       (vortex.skip.tests=true) where its Spark 3.5+ APIs are 
unavailable. -->
+                  <skipAddTestSource>${vortex.skip.tests}</skipAddTestSource>
+                  <sources>
+                    <source>src/test/java-vortex</source>
+                  </sources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/java-vortex/org/apache/hudi/io/storage/TestVortexReaderWriterRoundTrip.java
 
b/hudi-spark-datasource/hudi-spark/src/test/java-vortex/org/apache/hudi/io/storage/TestVortexReaderWriterRoundTrip.java
new file mode 100644
index 000000000000..8a8764f07d50
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/java-vortex/org/apache/hudi/io/storage/TestVortexReaderWriterRoundTrip.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.hudi.io.storage;
+
+import org.apache.hudi.HoodieSchemaConversionUtils;
+import org.apache.hudi.client.SparkTaskContextSupplier;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.catalyst.expressions.GenericInternalRow;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.unsafe.types.UTF8String;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end round-trip test for the Vortex file writer and reader against 
the real vortex-jni
+ * API. Writes InternalRows through {@link HoodieSparkVortexWriter} and reads 
them back through
+ * {@link HoodieSparkVortexReader}, spanning multiple flushed batches.
+ */
+@DisabledIfSystemProperty(named = "vortex.skip.tests", matches = "true")
+public class TestVortexReaderWriterRoundTrip {
+
+  @TempDir
+  File tempDir;
+
+  @Test
+  public void testWriteReadRoundTrip() throws Exception {
+    SparkTaskContextSupplier taskContextSupplier = new 
SparkTaskContextSupplier();
+
+    StructType schema = new StructType()
+        .add("id", DataTypes.LongType, false)
+        .add("name", DataTypes.StringType, false)
+        .add("score", DataTypes.DoubleType, false);
+
+    StoragePath path = new StoragePath(tempDir.getAbsolutePath() + 
"/roundtrip.vortex");
+
+    int numRows = 2500; // > default batch size (1000) to force multiple 
flushed batches
+    try (HoodieStorage storage = 
HoodieTestUtils.getStorage(tempDir.getAbsolutePath())) {
+      try (HoodieSparkVortexWriter writer = HoodieSparkVortexWriter.builder()
+          .file(path)
+          .sparkSchema(schema)
+          .instantTime("20251201120000000")
+          .taskContextSupplier(taskContextSupplier)
+          .storage(storage)
+          .populateMetaFields(false)
+          .build()) {
+        for (int i = 0; i < numRows; i++) {
+          InternalRow row = new GenericInternalRow(new Object[] {
+              (long) (100 + i), UTF8String.fromString("name" + i), (double) i 
+ 0.5});
+          writer.writeRow(row);
+        }
+      }
+
+      assertTrue(storage.exists(path), "Vortex file should exist");
+
+      try (HoodieSparkVortexReader reader = new HoodieSparkVortexReader(path)) 
{
+        assertEquals(numRows, reader.getTotalRecords(), "row count");
+
+        HoodieSchema readSchema = reader.getSchema();
+        assertEquals(3, readSchema.getFields().size(), "schema field count");
+
+        int count = 0;
+        try (ClosableIterator<HoodieRecord<InternalRow>> it = 
reader.getRecordIterator(readSchema)) {
+          while (it.hasNext()) {
+            InternalRow row = it.next().getData();
+            assertEquals(100L + count, row.getLong(0), "id at row " + count);
+            assertEquals("name" + count, row.getUTF8String(1).toString(), 
"name at row " + count);
+            assertEquals((double) count + 0.5, row.getDouble(2), 1e-9, "score 
at row " + count);
+            count++;
+          }
+        }
+        assertEquals(numRows, count, "rows read back");
+      }
+    }
+  }
+
+  @Test
+  public void testProjectedRead() throws Exception {
+    SparkTaskContextSupplier taskContextSupplier = new 
SparkTaskContextSupplier();
+
+    StructType schema = new StructType()
+        .add("id", DataTypes.LongType, false)
+        .add("name", DataTypes.StringType, false);
+
+    StoragePath path = new StoragePath(tempDir.getAbsolutePath() + 
"/projected.vortex");
+    try (HoodieStorage storage = 
HoodieTestUtils.getStorage(tempDir.getAbsolutePath())) {
+      try (HoodieSparkVortexWriter writer = HoodieSparkVortexWriter.builder()
+          .file(path).sparkSchema(schema).instantTime("20251201120000000")
+          
.taskContextSupplier(taskContextSupplier).storage(storage).populateMetaFields(false).build())
 {
+        for (int i = 0; i < 3; i++) {
+          writer.writeRow(new GenericInternalRow(new Object[] {(long) i, 
UTF8String.fromString("n" + i)}));
+        }
+      }
+
+      // Project only the non-leading "name" column so the assertion fails if 
the reader returns the
+      // full row instead of narrowing to the requested schema (projecting the 
leading "id" column
+      // would read back at index 0 either way and could not detect that 
regression).
+      HoodieSchema projected = 
HoodieSchemaConversionUtils.convertStructTypeToHoodieSchema(
+          new StructType().add("name", DataTypes.StringType, false), "record", 
"", false);
+      try (HoodieSparkVortexReader reader = new HoodieSparkVortexReader(path);
+           ClosableIterator<HoodieRecord<InternalRow>> it = 
reader.getRecordIterator(projected)) {
+        int count = 0;
+        while (it.hasNext()) {
+          InternalRow row = it.next().getData();
+          assertEquals("n" + count, row.getUTF8String(0).toString(), 
"projected name at row " + count);
+          count++;
+        }
+        assertEquals(3, count, "projected rows");
+      }
+    }
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/common/table/read/TestHoodieFileGroupReaderOnSpark.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/common/table/read/TestHoodieFileGroupReaderOnSpark.scala
index 8c4577192641..243ea0ade045 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/common/table/read/TestHoodieFileGroupReaderOnSpark.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/common/table/read/TestHoodieFileGroupReaderOnSpark.scala
@@ -116,7 +116,8 @@ class TestHoodieFileGroupReaderOnSpark extends 
TestHoodieFileGroupReaderBase[Int
     val dataSchema = 
HoodieSchemaConversionUtils.convertHoodieSchemaToStructType(schema)
     val orcReader = sparkAdapter.createOrcFileReader(vectorized = false, 
spark.sessionState.conf, Map.empty, 
storageConf.unwrapAs(classOf[Configuration]), dataSchema)
     val lanceReader = sparkAdapter.createLanceFileReader(vectorized = false, 
spark.sessionState.conf, Map.empty, 
storageConf.unwrapAs(classOf[Configuration])).orNull
-    val multiFormatReader = new 
MultipleColumnarFileFormatReader(parquetReader, orcReader, lanceReader)
+    val vortexReader = sparkAdapter.createVortexFileReader(vectorized = false, 
spark.sessionState.conf, Map.empty, 
storageConf.unwrapAs(classOf[Configuration])).orNull
+    val multiFormatReader = new 
MultipleColumnarFileFormatReader(parquetReader, orcReader, lanceReader, 
vortexReader)
     new SparkFileFormatInternalRowReaderContext(multiFormatReader, Seq.empty, 
Seq.empty, getStorageConf, metaClient.getTableConfig)
   }
 
diff --git 
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_3Adapter.scala
 
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_3Adapter.scala
index 25553f8ebdd7..b406668b908b 100644
--- 
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_3Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_3Adapter.scala
@@ -165,6 +165,13 @@ class Spark3_3Adapter extends BaseSpark3Adapter {
     None
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    None
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.stop()
   }
diff --git 
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_4Adapter.scala
 
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_4Adapter.scala
index 3d62ba1edc0c..93841ecf5b4c 100644
--- 
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_4Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_4Adapter.scala
@@ -166,6 +166,14 @@ class Spark3_4Adapter extends BaseSpark3Adapter {
     Some(new SparkLanceReaderBase(vectorized))
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    // Vortex Spark connector artifact is not available for Spark 3.4.
+    None
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.sc.stop(exitCode)
   }
diff --git 
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_5Adapter.scala
 
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_5Adapter.scala
index 3fd01ae7085b..5cad378dc736 100644
--- 
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_5Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/adapter/Spark3_5Adapter.scala
@@ -181,6 +181,15 @@ class Spark3_5Adapter extends BaseSpark3Adapter {
     Some(new SparkLanceReaderBase(vectorized))
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    // Vortex requires Java 17 (vortex-jni ships Java 17 bytecode) and is 
wired only through the
+    // Spark 4.x adapters. Spark 3.5 runs on Java 11, where 
SparkVortexReaderBase is not compiled.
+    None
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.sc.stop(exitCode)
   }
diff --git 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
index 519ec97cd771..f622b266db9f 100644
--- 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_0Adapter.scala
@@ -45,6 +45,7 @@ import org.apache.spark.sql.execution.datasources._
 import org.apache.spark.sql.execution.datasources.lance.SparkLanceReaderBase
 import 
org.apache.spark.sql.execution.datasources.parquet.{HoodieParquetReadSupport, 
ParquetFileFormat, Spark40HoodieParquetReadSupport, 
Spark40LegacyHoodieParquetFileFormat, Spark40ParquetReader}
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.execution.datasources.vortex.SparkVortexReaderBase
 import org.apache.spark.sql.execution.streaming.MemoryStream
 import org.apache.spark.sql.hudi.HoodieMemoryStream
 import org.apache.spark.sql.hudi.analysis.TableValuedFunctions
@@ -205,6 +206,13 @@ class Spark4_0Adapter extends BaseSpark4Adapter {
     Some(new SparkLanceReaderBase(vectorized))
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    Some(new SparkVortexReaderBase(vectorized))
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.sc.stop(exitCode)
   }
diff --git 
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_1Adapter.scala
 
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_1Adapter.scala
index f6a8e19b0e6c..363d0b64446a 100644
--- 
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_1Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_1Adapter.scala
@@ -45,6 +45,7 @@ import org.apache.spark.sql.execution.datasources._
 import org.apache.spark.sql.execution.datasources.lance.SparkLanceReaderBase
 import org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, 
Spark41LegacyHoodieParquetFileFormat, Spark41ParquetReader}
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.execution.datasources.vortex.SparkVortexReaderBase
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
 import org.apache.spark.sql.hudi.{HoodieMemoryStream, SparkAdapter}
 import org.apache.spark.sql.hudi.analysis.TableValuedFunctions
@@ -196,6 +197,13 @@ class Spark4_1Adapter extends BaseSpark4Adapter {
     Some(new SparkLanceReaderBase(vectorized))
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    Some(new SparkVortexReaderBase(vectorized))
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.sc.stop(exitCode)
   }
diff --git 
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_2Adapter.scala
 
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_2Adapter.scala
index a0fc4e5ee843..b2790091b5fd 100644
--- 
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_2Adapter.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/adapter/Spark4_2Adapter.scala
@@ -45,6 +45,7 @@ import org.apache.spark.sql.execution.datasources._
 import org.apache.spark.sql.execution.datasources.lance.SparkLanceReaderBase
 import org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, 
Spark42LegacyHoodieParquetFileFormat, Spark42ParquetReader}
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.execution.datasources.vortex.SparkVortexReaderBase
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
 import org.apache.spark.sql.hudi.{HoodieMemoryStream, SparkAdapter}
 import org.apache.spark.sql.hudi.analysis.TableValuedFunctions
@@ -196,6 +197,13 @@ class Spark4_2Adapter extends BaseSpark4Adapter {
     Some(new SparkLanceReaderBase(vectorized))
   }
 
+  override def createVortexFileReader(vectorized: Boolean,
+                                      sqlConf: SQLConf,
+                                      options: Map[String, String],
+                                      hadoopConf: Configuration): 
Option[SparkColumnarFileReader] = {
+    Some(new SparkVortexReaderBase(vectorized))
+  }
+
   override def stopSparkContext(jssc: JavaSparkContext, exitCode: Int): Unit = 
{
     jssc.sc.stop(exitCode)
   }
diff --git a/pom.xml b/pom.xml
index 2523a3c7b5f6..4ae9a2309cf3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -253,6 +253,9 @@
     <lance.spark.connector.version>0.4.0</lance.spark.connector.version>
     
<lance.spark.artifact>lance-spark-3.5_${scala.binary.version}</lance.spark.artifact>
     <lance.skip.tests>false</lance.skip.tests>
+    <vortex.version>0.76.0</vortex.version>
+    
<vortex.spark.artifact>vortex-spark_${scala.binary.version}</vortex.spark.artifact>
+    <vortex.skip.tests>false</vortex.skip.tests>
     <!-- The following properties are only used for Jacoco coverage report 
aggregation -->
     <copy.files>false</copy.files>
     
<copy.files.target.dir>${maven.multiModuleProjectDirectory}</copy.files.target.dir>
@@ -525,6 +528,7 @@
             <systemPropertyVariables>
               
<log4j.configurationFile>${surefire-log4j.file}</log4j.configurationFile>
               <lance.skip.tests>${lance.skip.tests}</lance.skip.tests>
+              <vortex.skip.tests>${vortex.skip.tests}</vortex.skip.tests>
             </systemPropertyVariables>
             <useSystemClassLoader>false</useSystemClassLoader>
             
<forkedProcessExitTimeoutInSeconds>30</forkedProcessExitTimeoutInSeconds>
@@ -569,6 +573,7 @@
             <systemProperties>
               
<log4j.configurationFile>${surefire-log4j.file}</log4j.configurationFile>
               <lance.skip.tests>${lance.skip.tests}</lance.skip.tests>
+              <vortex.skip.tests>${vortex.skip.tests}</vortex.skip.tests>
             </systemProperties>
           </configuration>
           <executions>
@@ -996,6 +1001,24 @@
           </exclusion>
         </exclusions>
       </dependency>
+      <!-- Vortex JNI -->
+      <dependency>
+        <groupId>dev.vortex</groupId>
+        <artifactId>vortex-jni</artifactId>
+        <version>${vortex.version}</version>
+      </dependency>
+      <!-- Vortex Spark connector -->
+      <dependency>
+        <groupId>dev.vortex</groupId>
+        <artifactId>${vortex.spark.artifact}</artifactId>
+        <version>${vortex.version}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>dev.vortex</groupId>
+            <artifactId>vortex-jni</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
       <!-- Apache Arrow -->
       <dependency>
         <groupId>org.apache.arrow</groupId>
@@ -2633,6 +2656,8 @@
         <!-- Lance: Skip tests for Spark 3.3 due to lack of support -->
         
<lance.spark.artifact>lance-spark-base_${scala.binary.version}</lance.spark.artifact>
         <lance.skip.tests>true</lance.skip.tests>
+        <!-- Vortex: Skip tests for Spark 3.3 due to lack of support -->
+        <vortex.skip.tests>true</vortex.skip.tests>
         <!-- NOTE: Some Hudi modules require standalone Parquet/Orc/etc 
file-format dependency (hudi-hive-sync,
                    hudi-hadoop-mr, for ex). Since these Hudi modules might be 
used from w/in the execution engine(s)
                    bringing these file-formats as dependencies as well, we 
need to make sure that versions are
@@ -2678,6 +2703,8 @@
         <!-- Lance: Use Spark 3.4-specific artifact -->
         
<lance.spark.artifact>lance-spark-3.4_${scala.binary.version}</lance.spark.artifact>
         <lance.skip.tests>true</lance.skip.tests>
+        <!-- Vortex: Skip tests for Spark 3.4 due to lack of support -->
+        <vortex.skip.tests>true</vortex.skip.tests>
         <!-- NOTE: Some Hudi modules require standalone Parquet/Orc/etc 
file-format dependency (hudi-hive-sync,
                    hudi-hadoop-mr, for ex). Since these Hudi modules might be 
used from w/in the execution engine(s)
                    bringing these file-formats as dependencies as well, we 
need to make sure that versions are
@@ -2733,6 +2760,9 @@
         <!-- Lance: Use Spark 3.5-specific artifact -->
         
<lance.spark.artifact>lance-spark-3.5_${scala.binary.version}</lance.spark.artifact>
         <lance.skip.tests>false</lance.skip.tests>
+        <!-- Vortex: Use Spark 3.5 artifact -->
+        
<vortex.spark.artifact>vortex-spark_${scala.binary.version}</vortex.spark.artifact>
+        <vortex.skip.tests>false</vortex.skip.tests>
         <!-- NOTE: Some Hudi modules require standalone Parquet/Orc/etc 
file-format dependency (hudi-hive-sync,
                    hudi-hadoop-mr, for ex). Since these Hudi modules might be 
used from w/in the execution engine(s)
                    bringing these file-formats as dependencies as well, we 
need to make sure that versions are
@@ -2796,6 +2826,9 @@
         <!-- Lance: Use Spark 4.0-specific artifact (Scala 2.13 only) -->
         <lance.spark.artifact>lance-spark-4.0_2.13</lance.spark.artifact>
         <lance.skip.tests>false</lance.skip.tests>
+        <!-- Vortex: Use Spark 4.0 artifact (Scala 2.13 only) -->
+        <vortex.spark.artifact>vortex-spark_2.13</vortex.spark.artifact>
+        <vortex.skip.tests>false</vortex.skip.tests>
         <!-- NOTE: Some Hudi modules require standalone Parquet/Orc/etc 
file-format dependency (hudi-hive-sync,
                    hudi-hadoop-mr, for ex). Since these Hudi modules might be 
used from w/in the execution engine(s)
                    bringing these file-formats as dependencies as well, we 
need to make sure that versions are
@@ -2859,6 +2892,9 @@
         <hive.storage.version>2.8.1</hive.storage.version>
         <lance.spark.artifact>lance-spark-4.1_2.13</lance.spark.artifact>
         <lance.skip.tests>false</lance.skip.tests>
+        <!-- Vortex: Use Spark 4.1 artifact (Scala 2.13 only) -->
+        <vortex.spark.artifact>vortex-spark_2.13</vortex.spark.artifact>
+        <vortex.skip.tests>false</vortex.skip.tests>
         <!-- NOTE: Some Hudi modules require standalone Parquet/Orc/etc 
file-format dependency (hudi-hive-sync,
                    hudi-hadoop-mr, for ex). Since these Hudi modules might be 
used from w/in the execution engine(s)
                    bringing these file-formats as dependencies as well, we 
need to make sure that versions are


Reply via email to