This is an automated email from the ASF dual-hosted git repository.
peterxcli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 891cf0e86fe HDDS-14945. Implement Iceberg position delete file rewrite
for path migration (#10306)
891cf0e86fe is described below
commit 891cf0e86fe9b0bec15f86be60db28d1b553c1d3
Author: sreejasahithi <[email protected]>
AuthorDate: Tue Jun 2 21:31:00 2026 +0530
HDDS-14945. Implement Iceberg position delete file rewrite for path
migration (#10306)
---
hadoop-ozone/dist/src/main/license/bin/LICENSE.txt | 14 ++
hadoop-ozone/dist/src/main/license/jar-report.txt | 14 ++
hadoop-ozone/iceberg/pom.xml | 105 +++++++++++
.../ozone/iceberg/RewriteTablePathOzoneAction.java | 208 ++++++++++++++++++++-
.../iceberg/TestRewriteTablePathOzoneAction.java | 194 ++++++++++++++++++-
5 files changed, 526 insertions(+), 9 deletions(-)
diff --git a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt
b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt
index e3b97ca202a..736374acf2a 100644
--- a/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt
+++ b/hadoop-ozone/dist/src/main/license/bin/LICENSE.txt
@@ -213,6 +213,7 @@ EDL 1.0
com.sun.activation:jakarta.activation
jakarta.activation:jakarta.activation-api
jakarta.xml.bind:jakarta.xml.bind-api
+ org.locationtech.jts:jts-core
EPL 2.0
@@ -398,6 +399,7 @@ Apache License 2.0
org.apache.hadoop:hadoop-common
org.apache.hadoop:hadoop-hdfs
org.apache.hadoop:hadoop-hdfs-client
+ org.apache.hadoop:hadoop-mapreduce-client-core
org.apache.hadoop:hadoop-shaded-guava
org.apache.hadoop:hadoop-shaded-protobuf_3_25
org.apache.httpcomponents:httpcore
@@ -405,6 +407,8 @@ Apache License 2.0
org.apache.iceberg:iceberg-bundled-guava
org.apache.iceberg:iceberg-common
org.apache.iceberg:iceberg-core
+ org.apache.iceberg:iceberg-orc
+ org.apache.iceberg:iceberg-parquet
org.apache.kerby:kerb-admin
org.apache.kerby:kerb-client
org.apache.kerby:kerb-common
@@ -422,6 +426,16 @@ Apache License 2.0
org.apache.kerby:token-provider
org.apache.logging.log4j:log4j-api
org.apache.logging.log4j:log4j-core
+ org.apache.orc:orc-core
+ org.apache.orc:orc-shims
+ org.apache.parquet:parquet-avro
+ org.apache.parquet:parquet-column
+ org.apache.parquet:parquet-common
+ org.apache.parquet:parquet-encoding
+ org.apache.parquet:parquet-format-structures
+ org.apache.parquet:parquet-hadoop
+ org.apache.parquet:parquet-jackson
+ org.apache.parquet:parquet-variant
org.apache.ranger:ranger-audit-core
org.apache.ranger:ranger-authz-api
org.apache.ranger:ranger-intg
diff --git a/hadoop-ozone/dist/src/main/license/jar-report.txt
b/hadoop-ozone/dist/src/main/license/jar-report.txt
index fc3bdf32e84..8209c270f07 100644
--- a/hadoop-ozone/dist/src/main/license/jar-report.txt
+++ b/hadoop-ozone/dist/src/main/license/jar-report.txt
@@ -65,6 +65,7 @@ share/ozone/lib/hadoop-auth.jar
share/ozone/lib/hadoop-common.jar
share/ozone/lib/hadoop-hdfs-client.jar
share/ozone/lib/hadoop-hdfs.jar
+share/ozone/lib/hadoop-mapreduce-client-core.jar
share/ozone/lib/hadoop-shaded-guava.jar
share/ozone/lib/hadoop-shaded-protobuf_3_25.jar
share/ozone/lib/hdds-cli-common.jar
@@ -91,6 +92,8 @@ share/ozone/lib/iceberg-api.jar
share/ozone/lib/iceberg-bundled-guava.jar
share/ozone/lib/iceberg-common.jar
share/ozone/lib/iceberg-core.jar
+share/ozone/lib/iceberg-orc.jar
+share/ozone/lib/iceberg-parquet.jar
share/ozone/lib/istack-commons-runtime.jar
share/ozone/lib/j2objc-annotations.jar
share/ozone/lib/jackson-annotations.jar
@@ -166,6 +169,7 @@ share/ozone/lib/json-simple.jar
share/ozone/lib/jsp-api.jar
share/ozone/lib/jspecify.jar
share/ozone/lib/jsr311-api.jar
+share/ozone/lib/jts-core.jar
share/ozone/lib/jtokkit.jar
share/ozone/lib/kerb-core.jar
share/ozone/lib/kerb-crypto.jar
@@ -226,6 +230,8 @@ share/ozone/lib/opentelemetry-sdk-logs.jar
share/ozone/lib/opentelemetry-sdk-metrics.jar
share/ozone/lib/opentelemetry-sdk-trace.jar
share/ozone/lib/opentelemetry-sdk.jar
+share/ozone/lib/orc-core-nohive.jar
+share/ozone/lib/orc-shims.jar
share/ozone/lib/osgi-resource-locator.jar
share/ozone/lib/ozone-cli-admin.jar
share/ozone/lib/ozone-cli-debug.jar
@@ -254,6 +260,14 @@ share/ozone/lib/ozone-s3-secret-store.jar
share/ozone/lib/ozone-s3gateway.jar
share/ozone/lib/ozone-tools.jar
share/ozone/lib/ozone-vapor.jar
+share/ozone/lib/parquet-avro.jar
+share/ozone/lib/parquet-column.jar
+share/ozone/lib/parquet-common.jar
+share/ozone/lib/parquet-encoding.jar
+share/ozone/lib/parquet-format-structures.jar
+share/ozone/lib/parquet-hadoop.jar
+share/ozone/lib/parquet-jackson.jar
+share/ozone/lib/parquet-variant.jar
share/ozone/lib/perfmark-api.jar
share/ozone/lib/picocli-shell-jline3.jar
share/ozone/lib/picocli.jar
diff --git a/hadoop-ozone/iceberg/pom.xml b/hadoop-ozone/iceberg/pom.xml
index d7b822c3860..988f7bc6f66 100644
--- a/hadoop-ozone/iceberg/pom.xml
+++ b/hadoop-ozone/iceberg/pom.xml
@@ -32,6 +32,11 @@
</properties>
<dependencies>
+ <dependency>
+ <groupId>org.apache.avro</groupId>
+ <artifactId>avro</artifactId>
+ <version>1.12.0</version>
+ </dependency>
<!-- Iceberg dependencies -->
<dependency>
@@ -64,11 +69,111 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.iceberg</groupId>
+ <artifactId>iceberg-orc</artifactId>
+ <version>${iceberg.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.iceberg</groupId>
+ <artifactId>iceberg-parquet</artifactId>
+ <version>${iceberg.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>commons-pool</groupId>
+ <artifactId>commons-pool</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.orc</groupId>
+ <artifactId>orc-core</artifactId>
+ <version>1.9.7</version>
+ <classifier>nohive</classifier>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-client-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.threeten</groupId>
+ <artifactId>threeten-extra</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.parquet</groupId>
+ <artifactId>parquet-column</artifactId>
+ <version>1.16.0</version>
+ </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-mapreduce-client-core</artifactId>
+ <version>${hadoop.version}</version>
+ <scope>runtime</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>com.github.pjfanning</groupId>
+ <artifactId>jersey-json</artifactId>
+ </exclusion>
+
+ <!-- Jersey -->
+ <exclusion>
+ <groupId>com.sun.jersey</groupId>
+ <artifactId>jersey-guice</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.sun.jersey</groupId>
+ <artifactId>jersey-servlet</artifactId>
+ </exclusion>
+
+ <!-- JAXB -->
+ <exclusion>
+ <groupId>javax.xml.bind</groupId>
+ <artifactId>jaxb-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.avro</groupId>
+ <artifactId>avro</artifactId>
+ </exclusion>
+ <!-- Hadoop YARN -->
+ <exclusion>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-yarn-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-yarn-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-yarn-common</artifactId>
+ </exclusion>
+
+ <!-- Jetty websocket/client -->
+ <exclusion>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-common</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
diff --git
a/hadoop-ozone/iceberg/src/main/java/org/apache/hadoop/ozone/iceberg/RewriteTablePathOzoneAction.java
b/hadoop-ozone/iceberg/src/main/java/org/apache/hadoop/ozone/iceberg/RewriteTablePathOzoneAction.java
index 0327b7a5fd6..4dc50434a3c 100644
---
a/hadoop-ozone/iceberg/src/main/java/org/apache/hadoop/ozone/iceberg/RewriteTablePathOzoneAction.java
+++
b/hadoop-ozone/iceberg/src/main/java/org/apache/hadoop/ozone/iceberg/RewriteTablePathOzoneAction.java
@@ -46,18 +46,33 @@
import org.apache.iceberg.PartitionStatisticsFile;
import org.apache.iceberg.RewriteTablePathUtil;
import org.apache.iceberg.RewriteTablePathUtil.RewriteResult;
+import org.apache.iceberg.Schema;
import org.apache.iceberg.Snapshot;
import org.apache.iceberg.StaticTableOperations;
+import org.apache.iceberg.StructLike;
import org.apache.iceberg.Table;
import org.apache.iceberg.TableMetadata;
import org.apache.iceberg.TableMetadata.MetadataLogEntry;
import org.apache.iceberg.TableMetadataParser;
import org.apache.iceberg.actions.ImmutableRewriteTablePath;
import org.apache.iceberg.actions.RewriteTablePath;
+import org.apache.iceberg.avro.Avro;
+import org.apache.iceberg.data.Record;
+import org.apache.iceberg.data.avro.DataReader;
+import org.apache.iceberg.data.avro.DataWriter;
+import org.apache.iceberg.data.orc.GenericOrcReader;
+import org.apache.iceberg.data.orc.GenericOrcWriter;
+import org.apache.iceberg.data.parquet.GenericParquetReaders;
+import org.apache.iceberg.data.parquet.GenericParquetWriter;
+import org.apache.iceberg.deletes.PositionDeleteWriter;
import org.apache.iceberg.exceptions.RuntimeIOException;
import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.DeleteSchemaUtil;
import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.InputFile;
import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.orc.ORC;
+import org.apache.iceberg.parquet.Parquet;
import org.apache.iceberg.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -228,8 +243,6 @@ private boolean versionInFilePath(String path, String
version) {
}
private String rebuildMetadata() {
- // TODO: position delete file entries in rewriteManifestResult.copyPlan()
reference staging paths
- // that are never written, exclude them until position delete
rewriting is implemented.
TableMetadata startMetadata = startVersionName != null
? new StaticTableOperations(startVersionName, table.io()).current()
: null;
@@ -255,6 +268,13 @@ private String rebuildMetadata() {
RewriteContentFileResult rewriteManifestResult =
rewriteManifests(deltaSnapshotIds, endMetadata,
rewriteManifestListResult.toRewrite());
+ Set<DeleteFile> deleteFiles =
+ rewriteManifestResult.toRewrite().stream()
+ .filter(e -> e instanceof DeleteFile)
+ .map(e -> (DeleteFile) e)
+ .collect(Collectors.toSet());
+ rewritePositionDeletes(deleteFiles);
+
Set<Pair<String, String>> copyPlan = new HashSet<>();
copyPlan.addAll(rewriteVersionResult.copyPlan());
copyPlan.addAll(rewriteManifestListResult.copyPlan());
@@ -675,4 +695,188 @@ private static RewriteResult<DeleteFile>
writeDeleteManifest(
throw new RuntimeIOException(e);
}
}
+
+ static class OzonePositionDeleteReaderWriter implements
RewriteTablePathUtil.PositionDeleteReaderWriter {
+ @Override
+ public CloseableIterable<Record> reader(
+ InputFile inputFile, FileFormat format, PartitionSpec spec) {
+ return positionDeletesReader(inputFile, format, spec);
+ }
+
+ @Override
+ public PositionDeleteWriter<Record> writer(
+ OutputFile outputFile,
+ FileFormat format,
+ PartitionSpec spec,
+ StructLike partition,
+ Schema rowSchema)
+ throws IOException {
+ return positionDeletesWriter(outputFile, format, spec, partition,
rowSchema);
+ }
+ }
+
+ private void rewritePositionDeletes(Set<DeleteFile> toRewrite) {
+ /*
+ * NOTE: Rewriting position delete files updates embedded data file paths,
which changes the
+ * resulting file size. This causes a metadata mismatch in the manifests:
+ *
+ * 1. Dependency: Manifests MUST be rewritten first because they are the
source of truth used to identify which
+ * position delete files exist and need processing.
+ * 2. Issue: Because manifests are written before the delete files are
updated, the 'file_size_in_bytes' field
+ * in the manifest reflects the original size, not the new size.
+ * 3. Impact: Some catalogs (e.g., REST catalogs like Polaris) will fail
to read these files as the reader uses
+ * the stale size from the manifest.
+ *
+ * This is a known Iceberg limitation being addressed by the Iceberg
community. Once that fix is available
+ * in the Iceberg core, this action should be updated accordingly.
+ */
+ if (toRewrite.isEmpty()) {
+ return;
+ }
+
+ RewriteTablePathUtil.PositionDeleteReaderWriter posDeleteReaderWriter =
new OzonePositionDeleteReaderWriter();
+ int maxInFlight = parallelism * MAX_INFLIGHT_MULTIPLIER;
+ Semaphore semaphore = new Semaphore(maxInFlight);
+ ExecutorCompletionService<Void> completionService = new
ExecutorCompletionService<>(executorService);
+ int submittedTasks = 0;
+ int completedTasks = 0;
+
+ try {
+ for (DeleteFile deleteFile : toRewrite) {
+ semaphore.acquire();
+ boolean taskSubmitted = false;
+ try {
+ completionService.submit(() -> {
+ try {
+ rewritePositionDelete(deleteFile, table, sourcePrefix,
targetPrefix, stagingDir, posDeleteReaderWriter);
+ return null;
+ } finally {
+ semaphore.release();
+ }
+ });
+ taskSubmitted = true;
+ submittedTasks++;
+ } finally {
+ if (!taskSubmitted) {
+ semaphore.release();
+ }
+ }
+
+ Future<Void> done;
+ while ((done = completionService.poll()) != null) {
+ done.get();
+ completedTasks++;
+ }
+ }
+
+ while (completedTasks < submittedTasks) {
+ completionService.take().get();
+ completedTasks++;
+ }
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ executorService.shutdownNow();
+ throw new RuntimeException("Interrupted while rewriting position delete
files", e);
+
+ } catch (ExecutionException e) {
+ executorService.shutdownNow();
+ throw new RuntimeException("Failed to rewrite position delete file",
e.getCause());
+ }
+ }
+
+ private static void rewritePositionDelete(
+ DeleteFile deleteFile,
+ Table table,
+ String sourcePrefixArg,
+ String targetPrefixArg,
+ String stagingLocationArg,
+ RewriteTablePathUtil.PositionDeleteReaderWriter posDeleteReaderWriter) {
+ try {
+ FileIO io = table.io();
+ String newPath =
+ RewriteTablePathUtil.stagingPath(
+ deleteFile.location(), sourcePrefixArg, stagingLocationArg);
+ OutputFile outputFile = io.newOutputFile(newPath);
+ PartitionSpec spec = table.specs().get(deleteFile.specId());
+ RewriteTablePathUtil.rewritePositionDeleteFile(
+ deleteFile,
+ outputFile,
+ io,
+ spec,
+ sourcePrefixArg,
+ targetPrefixArg,
+ posDeleteReaderWriter);
+ } catch (IOException e) {
+ LOG.error("Failed to rewrite position delete file: {}",
+ deleteFile.location(), e);
+ throw new RuntimeIOException(e);
+ }
+ }
+
+ static CloseableIterable<Record> positionDeletesReader(
+ InputFile inputFile, FileFormat format, PartitionSpec spec) {
+ Schema deleteSchema = DeleteSchemaUtil.posDeleteReadSchema(spec.schema());
+ switch (format) {
+ case AVRO:
+ return Avro.read(inputFile)
+ .project(deleteSchema)
+ .reuseContainers()
+ .createReaderFunc(DataReader::create)
+ .build();
+
+ case PARQUET:
+ return Parquet.read(inputFile)
+ .project(deleteSchema)
+ .reuseContainers()
+ .createReaderFunc(
+ fileSchema -> GenericParquetReaders.buildReader(deleteSchema,
fileSchema))
+ .build();
+
+ case ORC:
+ return ORC.read(inputFile)
+ .project(deleteSchema)
+ .createReaderFunc(fileSchema ->
GenericOrcReader.buildReader(deleteSchema, fileSchema))
+ .build();
+
+ default:
+ LOG.error("Unsupported file format: {} for input file: {}", format,
inputFile.location());
+ throw new UnsupportedOperationException("Unsupported file format: " +
format);
+ }
+ }
+
+ static PositionDeleteWriter<Record> positionDeletesWriter(
+ OutputFile outputFile,
+ FileFormat format,
+ PartitionSpec spec,
+ StructLike partition,
+ Schema rowSchema)
+ throws IOException {
+ switch (format) {
+ case AVRO:
+ return Avro.writeDeletes(outputFile)
+ .createWriterFunc(DataWriter::create)
+ .withPartition(partition)
+ .rowSchema(rowSchema)
+ .withSpec(spec)
+ .buildPositionWriter();
+ case PARQUET:
+ return Parquet.writeDeletes(outputFile)
+ .createWriterFunc(GenericParquetWriter::create)
+ .withPartition(partition)
+ .rowSchema(rowSchema)
+ .withSpec(spec)
+ .buildPositionWriter();
+ case ORC:
+ return ORC.writeDeletes(outputFile)
+ .createWriterFunc(GenericOrcWriter::buildWriter)
+ .withPartition(partition)
+ .rowSchema(rowSchema)
+ .withSpec(spec)
+ .buildPositionWriter();
+ default:
+ LOG.error("Unsupported file format: {} for output file: {}", format,
outputFile.location());
+ throw new UnsupportedOperationException("Unsupported file format: " +
format);
+ }
+ }
}
diff --git
a/hadoop-ozone/iceberg/src/test/java/org/apache/hadoop/ozone/iceberg/TestRewriteTablePathOzoneAction.java
b/hadoop-ozone/iceberg/src/test/java/org/apache/hadoop/ozone/iceberg/TestRewriteTablePathOzoneAction.java
index 87c9c665ff8..5b11edc1a11 100644
---
a/hadoop-ozone/iceberg/src/test/java/org/apache/hadoop/ozone/iceberg/TestRewriteTablePathOzoneAction.java
+++
b/hadoop-ozone/iceberg/src/test/java/org/apache/hadoop/ozone/iceberg/TestRewriteTablePathOzoneAction.java
@@ -33,6 +33,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.hadoop.conf.Configuration;
import org.apache.iceberg.DataFile;
@@ -48,7 +49,11 @@
import org.apache.iceberg.ManifestFile;
import org.apache.iceberg.ManifestFiles;
import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.MetadataColumns;
+import org.apache.iceberg.MetricsConfig;
+import org.apache.iceberg.PartitionData;
import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.PartitionStatisticsFile;
import org.apache.iceberg.RewriteTablePathUtil;
import org.apache.iceberg.Schema;
import org.apache.iceberg.Snapshot;
@@ -57,14 +62,27 @@
import org.apache.iceberg.Table;
import org.apache.iceberg.TableMetadata;
import org.apache.iceberg.TableMetadata.MetadataLogEntry;
+import org.apache.iceberg.TableOperations;
import org.apache.iceberg.actions.RewriteTablePath;
+import org.apache.iceberg.data.GenericRecord;
+import org.apache.iceberg.data.Record;
+import org.apache.iceberg.data.parquet.GenericParquetReaders;
+import org.apache.iceberg.data.parquet.GenericParquetWriter;
+import org.apache.iceberg.deletes.PositionDelete;
+import org.apache.iceberg.deletes.PositionDeleteWriter;
import org.apache.iceberg.hadoop.HadoopTables;
import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.DeleteSchemaUtil;
+import org.apache.iceberg.io.InputFile;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.parquet.Parquet;
import org.apache.iceberg.types.Types;
import org.apache.iceberg.util.Pair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.Mockito;
/**
@@ -92,7 +110,7 @@ class TestRewriteTablePathOzoneAction {
private Path stagingDir;
@BeforeEach
- public void setupTableLocation() {
+ public void setupTableLocation() throws IOException {
String tableLocation =
tableDir.toUri().toString().replaceFirst("^file:///", "file:/") + TABLE_NAME;
this.table = createTable(tableLocation + "/");
this.sourcePrefix = tableLocation;
@@ -101,7 +119,7 @@ public void setupTableLocation() {
@Test
void fullTablePathRewrite() throws Exception {
- RewriteTablePath.Result result = new RewriteTablePathOzoneAction(table)
+ RewriteTablePath.Result result = new RewriteTablePathOzoneAction(table, 2)
.rewriteLocationPrefix(sourcePrefix, targetPrefix)
.stagingLocation(stagingDir.toString() + "/")
.execute();
@@ -305,9 +323,18 @@ void endVersionRejectsDeletedVersionFile() {
assertThat(exception).hasMessageContaining("does not exist");
}
+ @Test
+ void usesCurrentMetadataIfEndVersionNotProvided() {
+ String currentMetadata = ((HasTableOperations)
table).operations().current().metadataFileLocation();
+ RewriteTablePathOzoneAction action = new
RewriteTablePathOzoneAction(table);
+ action.rewriteLocationPrefix(sourcePrefix,
targetPrefix).stagingLocation(stagingDir + "/");
+ RewriteTablePath.Result result = action.execute();
+
assertThat(result.latestVersion()).isEqualTo(RewriteTablePathUtil.fileName(currentMetadata));
+ }
+
+ @Test
void defaultStagingDirIsUnderTableMetadataLocation() {
String metadataLocation =
RewriteTablePathOzoneUtils.getMetadataLocation(table);
-
RewriteTablePath.Result result = new RewriteTablePathOzoneAction(table)
.rewriteLocationPrefix(sourcePrefix, targetPrefix)
.execute();
@@ -366,6 +393,110 @@ void statsFileCopyPlanReturnsBeforeToAfterPathPairs() {
Pair.of("before-1.stats", "after-1.stats"),
Pair.of("before-2.stats", "after-2.stats")), copyPlan);
}
+
+ @Test
+ void rejectsTablesWithPartitionStatistics() {
+ TableMetadata baseMetadata = ((HasTableOperations)
table).operations().current();
+ long snapshotId = baseMetadata.currentSnapshot().snapshotId();
+ PartitionStatisticsFile statsFile =
Mockito.mock(PartitionStatisticsFile.class);
+ Mockito.when(statsFile.snapshotId()).thenReturn(snapshotId);
+ Mockito.when(statsFile.path()).thenReturn(sourcePrefix +
"/metadata/dummy.stats");
+ Mockito.when(statsFile.fileSizeInBytes()).thenReturn(100L);
+ TableMetadata metadataWithStats = TableMetadata.buildFrom(baseMetadata)
+ .setPartitionStatistics(statsFile)
+ .build();
+
+ TableOperations ops = ((HasTableOperations) table).operations();
+ ops.commit(baseMetadata, metadataWithStats);
+
+ RewriteTablePath action = new RewriteTablePathOzoneAction(table)
+ .rewriteLocationPrefix(sourcePrefix, targetPrefix)
+ .stagingLocation(stagingDir + "/");
+
+ IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, action::execute);
+ assertThat(exception).hasMessageContaining("Partition statistics files are
not supported yet.");
+ }
+
+ @Test
+ public void positionDeletesReaderUnsupportedFormat() {
+ InputFile mockInput = Mockito.mock(InputFile.class);
+ Mockito.when(mockInput.location()).thenReturn("s3://bucket/test.txt");
+ PartitionSpec spec = PartitionSpec.unpartitioned();
+ FileFormat mockUnsupportedFormat = Mockito.mock(FileFormat.class);
+ Mockito.when(mockUnsupportedFormat.toString()).thenReturn("txt");
+
+ UnsupportedOperationException exception =
assertThrows(UnsupportedOperationException.class,
+ () -> RewriteTablePathOzoneAction.positionDeletesReader(mockInput,
mockUnsupportedFormat, spec));
+
+ assertThat(exception).hasMessageContaining("Unsupported file format: txt");
+ }
+
+ @Test
+ public void positionDeletesWriterUnsupportedFormat() {
+ OutputFile mockOutput = Mockito.mock(OutputFile.class);
+ Mockito.when(mockOutput.location()).thenReturn("s3://bucket/test.txt");
+ PartitionSpec spec = PartitionSpec.unpartitioned();
+ FileFormat mockUnsupportedFormat = Mockito.mock(FileFormat.class);
+ Mockito.when(mockUnsupportedFormat.toString()).thenReturn("txt");
+
+ UnsupportedOperationException exception =
assertThrows(UnsupportedOperationException.class,
+ () -> RewriteTablePathOzoneAction.positionDeletesWriter(
+ mockOutput, mockUnsupportedFormat, spec, null, null));
+
+ assertThat(exception).hasMessageContaining("Unsupported file format: txt");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = FileFormat.class, names = {"AVRO", "ORC", "PARQUET"})
+ void positionDeletesAvroAndOrcRoundTrip(FileFormat format, @TempDir Path
temp) throws IOException {
+ String extension = format.name().toLowerCase();
+ String path = temp.resolve("test." + extension).toUri().toString();
+ OutputFile outputFile = table.io().newOutputFile(path);
+ PartitionSpec spec = table.spec();
+
+ try (PositionDeleteWriter<Record> writer =
RewriteTablePathOzoneAction.positionDeletesWriter(
+ outputFile, format, spec, new PartitionData(spec.partitionType()),
SCHEMA)) {
+
+ GenericRecord row = GenericRecord.create(SCHEMA);
+ row.setField("c1", 42);
+ row.setField("c2", format.name() + "-test");
+
+ writer.write(PositionDelete.<Record>create().set("data.parquet", 100L,
row));
+ }
+
+ try (CloseableIterable<Record> reader =
RewriteTablePathOzoneAction.positionDeletesReader(
+ table.io().newInputFile(path), format, spec)) {
+
+ List<Record> results = new ArrayList<>();
+ reader.forEach(results::add);
+
+ assertThat(results).hasSize(1);
+ Record record = results.get(0);
+
+
assertThat(record.getField("file_path").toString()).isEqualTo("data.parquet");
+ assertThat(record.getField("pos")).isEqualTo(100L);
+
+ Record rowResult = (Record) record.getField("row");
+ assertThat(rowResult.getField("c1")).isEqualTo(42);
+ assertThat(rowResult.getField("c2")).isEqualTo(format.name() + "-test");
+ }
+ }
+
+ @Test
+ void manifestsToRewriteRejectsMissingManifestList() {
+ Snapshot snapshot = table.currentSnapshot();
+ String manifestListLocation = snapshot.manifestListLocation();
+ table.io().deleteFile(manifestListLocation);
+
+ RewriteTablePath action = new RewriteTablePathOzoneAction(table)
+ .rewriteLocationPrefix(sourcePrefix, targetPrefix)
+ .stagingLocation(stagingDir + "/");
+
+ RuntimeException exception = assertThrows(RuntimeException.class,
action::execute);
+ assertThat(exception).hasMessageContaining("Failed to collect manifests to
rewrite");
+ assertThat(exception.getCause()).hasMessageContaining("Failed to read
manifests for snapshot " +
+ snapshot.snapshotId());
+ }
/**
* For every staged file in the CSV copy plan, asserts that internal paths
are rewritten
@@ -375,8 +506,8 @@ void statsFileCopyPlanReturnsBeforeToAfterPathPairs() {
* manifest-list references all start with target.</li>
* <li><b>snap-*.avro (manifest-list)</b>: target path starts with target,
and every
* manifest entry path inside the staged file starts with target.</li>
- * <li><b>*.avro (manifest)</b>: target path starts with target (content
rewrite
- * is not yet implemented).</li>
+ * <li><b>*.avro (manifest)</b>: target path starts with target and the
content inside it.</li>
+ * <li><b>deletes.parquet(position delete file)</b>: target path starts
with target and the content inside it.</li>
* </ul>
*/
private void assertAllInternalPathsRewritten(Set<Pair<String, String>>
csvPairs, String target) throws Exception {
@@ -392,6 +523,8 @@ private void
assertAllInternalPathsRewritten(Set<Pair<String, String>> csvPairs,
} else if (RewriteTablePathUtil.fileName(stagingPath).endsWith(".avro"))
{
assertTrue(targetPath.startsWith(target),
"Manifest file target path should start with target prefix: " +
targetPath);
+ } else if (stagingPath.endsWith("deletes.parquet")) {
+ assertStagedDeleteFileInternalPathsRewritten(table, stagingPath,
target);
}
}
}
@@ -538,6 +671,27 @@ private void
assertDeleteManifestPathsRewritten(ManifestFile staged, ManifestFil
"Rewritten delete manifest should reference the same delete files (by
name) as the original");
}
+ private static void assertStagedDeleteFileInternalPathsRewritten(
+ Table tbl, String stagedPath, String targetPrefix) throws IOException {
+ Schema readSchema = DeleteSchemaUtil.pathPosSchema();
+ String pathColumn = MetadataColumns.DELETE_FILE_PATH.name();
+ int rowCount = 0;
+ try (CloseableIterable<Record> rows =
+ Parquet.read(tbl.io().newInputFile(stagedPath))
+ .project(readSchema)
+ .createReaderFunc(fileSchema ->
GenericParquetReaders.buildReader(readSchema, fileSchema))
+ .build()) {
+ for (Record row : rows) {
+ Object path = row.getField(pathColumn);
+ assertTrue(
+ path.toString().startsWith(targetPrefix),
+ stagedPath + " row " + rowCount + ": path '" + path
+ + "' must start with '" + targetPrefix + "'");
+ rowCount++;
+ }
+ }
+ }
+
private static List<String> metadataLogEntryPaths(Table tbl) {
TableMetadata meta = ((HasTableOperations) tbl).operations().current();
List<String> paths = new ArrayList<>();
@@ -569,13 +723,19 @@ private static Set<Pair<String, String>>
readCsvPairs(Table tbl, String fileList
return pairs;
}
- private Table createTable(String location) {
+ private Table createTable(String location) throws IOException {
HadoopTables tables = new HadoopTables(new Configuration());
Table tbl = tables.create(SCHEMA, PartitionSpec.unpartitioned(), new
HashMap<>(), location);
for (int i = 0; i < COMMITS; i++) {
- String dataPath = location + "/data/batch-" + i + ".parquet";
+ String dataPath = location + "data/batch-" + i + ".parquet";
tbl.newAppend().appendFile(dummyDataFile(dataPath)).commit();
}
+
+ for (int i = 0; i < 2; i++) {
+ String dataPath = location + "data/batch-" + i + ".parquet";
+ DeleteFile df = writePositionDeleteFile(tbl, dataPath);
+ tbl.newRowDelta().addDeletes(df).commit();
+ }
return tables.load(location);
}
@@ -588,6 +748,26 @@ private DataFile dummyDataFile(String dataPath) {
.build();
}
+ private DeleteFile writePositionDeleteFile(Table tbl, String
referencedDataPath)
+ throws IOException {
+ String deleteUri = RewriteTablePathUtil.combinePaths(
+ tbl.location(), "data/" + UUID.randomUUID() + "-deletes.parquet");
+ PositionDeleteWriter<Record> writer =
+ Parquet.writeDeletes(tbl.io().newOutputFile(deleteUri))
+ .createWriterFunc(GenericParquetWriter::create)
+ .withSpec(tbl.spec())
+ .withPartition(new PartitionData(tbl.spec().partitionType()))
+ .metricsConfig(MetricsConfig.forPositionDelete(tbl))
+ .overwrite()
+ .buildPositionWriter();
+ try {
+ writer.write(PositionDelete.<Record>create().set(referencedDataPath,
0L));
+ } finally {
+ writer.close();
+ }
+ return writer.toDeleteFile();
+ }
+
private static StatisticsFile statisticsFile(String path, long
fileSizeInBytes) {
return new GenericStatisticsFile(1L, path, fileSizeInBytes, 0L, List.of());
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]