Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
73e38e5
Cannot pass tests for unknown reason
JonasJ-ap Dec 18, 2022
5544f45
fix test config issue and formatting
JonasJ-ap Dec 18, 2022
b8b6119
transfer icebergCatalog to parent class
JonasJ-ap Dec 18, 2022
274560c
fix formatting
JonasJ-ap Dec 18, 2022
39e3541
implement direct schema transformation and stop using spark context t…
JonasJ-ap Dec 19, 2022
92f962c
refactor and simplify the implementation
JonasJ-ap Dec 19, 2022
033c997
no need to make spark utils public
JonasJ-ap Dec 19, 2022
681a32f
simplify the implementation in Spark Action
JonasJ-ap Dec 19, 2022
77bdb27
add support for scala 2.13
JonasJ-ap Dec 19, 2022
3dd540a
add format check for data files
JonasJ-ap Dec 19, 2022
27ece93
fix naming issue
JonasJ-ap Dec 19, 2022
a9faabf
make delta type visitor abstract
JonasJ-ap Dec 21, 2022
3982711
fix typo and nit problems
JonasJ-ap Dec 21, 2022
9a7c443
migrate from iceberg-core to delta-lake
JonasJ-ap Dec 21, 2022
173534e
move get Metrics for File to iceberg-delta-lake
JonasJ-ap Dec 21, 2022
bdd1ccf
fix comment
JonasJ-ap Dec 21, 2022
85abac2
fix wrong import
JonasJ-ap Dec 21, 2022
32e1af8
Migrate delta to iceberg round 1 (#29)
JonasJ-ap Dec 24, 2022
ac1141d
Migrate delta to iceberg util refactor (#30)
JonasJ-ap Dec 25, 2022
8e9b3fc
Migrate delta to iceberg refactor 1.5 (#31)
JonasJ-ap Dec 28, 2022
12b60ca
Merge branch 'master' into migrate_delta_to_iceberg
JonasJ-ap Dec 28, 2022
8a8adef
use transaction, refactor structure, add optional newTableLocation, a…
JonasJ-ap Dec 29, 2022
6fbf740
fix the potential path error due to ambiguous return value of AddFile…
JonasJ-ap Dec 29, 2022
69671b9
refactor getFullPath with unit tests, use newCreateTableTransaction, …
JonasJ-ap Dec 30, 2022
e3138a6
allow user to specify a custom location for migrated table, fix load …
JonasJ-ap Jan 3, 2023
2e8dfd0
Fix nit problems and optimize some implementation (#38)
JonasJ-ap Jan 6, 2023
f4589e8
optimize the constructor to be more clean
JonasJ-ap Jan 6, 2023
59c96cb
move everthing to iceberg-delta-lake, build demo integration test (#35)
JonasJ-ap Jan 7, 2023
afd783b
optimize api structure, refactor the integration test, add more tests…
JonasJ-ap Jan 9, 2023
5b95925
refactor the interfaces, add new tests to integration tests, add new …
JonasJ-ap Jan 10, 2023
f43c325
fix error messages and add default implementation for actionProvider …
JonasJ-ap Jan 10, 2023
b2a8bfe
refactor the default implementation and javadoc (#43)
JonasJ-ap Jan 10, 2023
450a08c
fix error when migrating table with nested fields, add CI, upgrade te…
JonasJ-ap Jan 12, 2023
300d39b
remove unused helper class in test
JonasJ-ap Jan 12, 2023
a285c4a
add null check for stopMetastoreAndSpark, remove unnecessary try-catch
JonasJ-ap Jan 12, 2023
5760a83
use assertions.assertThatThrownBy to test precondition checks
JonasJ-ap Jan 12, 2023
e41c787
use assertThat to fix assert.True in TestDeltaLakeTypeToType
JonasJ-ap Jan 12, 2023
7a16809
use AssertionJ in TestSnapshotDeltaLakeTable
JonasJ-ap Jan 13, 2023
7072612
fix format and nit issue
JonasJ-ap Jan 13, 2023
c2293c9
remove unnecessary fields and class and let integrationTest collected…
JonasJ-ap Jan 13, 2023
f38d7b1
remove unnecessary try catch
JonasJ-ap Jan 13, 2023
10163f8
fix wrong modifier of a private method
JonasJ-ap Jan 13, 2023
99dbba8
Merge remote-tracking branch 'origin/master' into migrate_delta_to_ic…
JonasJ-ap Jan 15, 2023
a7c3de1
simplify the test base (#46)
JonasJ-ap Jan 17, 2023
6c4ab2c
save getLength Call for AddFile and when RemoveFile contains size (#47)
JonasJ-ap Jan 20, 2023
dadd76a
add null check for table.currentSnapshot() when querying the total nu…
JonasJ-ap Jan 21, 2023
1cd36b9
Refactor iceberg-delta's integration test(#48)
JonasJ-ap Jan 24, 2023
4463f30
Adapt for delta.logRetentionDuration (#49)
JonasJ-ap Jan 25, 2023
d3ccc86
fix comment and format issue
JonasJ-ap Jan 26, 2023
1affcb3
remove support for avro, orc since it can allow use to get rid of a d…
JonasJ-ap Feb 6, 2023
098a3a2
using resolvingFileIO instead
JonasJ-ap Feb 6, 2023
f0d1536
Merge remote-tracking branch 'origin/master' into migrate_delta_to_ic…
JonasJ-ap Feb 6, 2023
a98461a
rollback to hadoopFileIO
JonasJ-ap Feb 7, 2023
fe6da17
add test for array of structs
JonasJ-ap Feb 7, 2023
8e9a3e2
use Do not Support instead of cannot determine, remove support for av…
JonasJ-ap Feb 7, 2023
24405e0
nit fix
JonasJ-ap Feb 7, 2023
c5a6186
error message nit fix
JonasJ-ap Feb 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ default MigrateTable migrateTable(String tableIdent) {
this.getClass().getName() + " does not implement migrateTable");
}

default MigrateDeltaLakeTable migrateDeltaLakeTable(String tableIdent, String deltaS3Location) {
Comment thread
jackye1995 marked this conversation as resolved.
Outdated
throw new UnsupportedOperationException(
this.getClass().getName() + " does not implement migrateDeltaLakeTable");
}

/** Instantiates an action to delete orphan files. */
default DeleteOrphanFiles deleteOrphanFiles(Table table) {
throw new UnsupportedOperationException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.iceberg.actions;

import java.util.Map;

/** Migrates a Delta Lake table to Iceberg in place. */
public interface MigrateDeltaLakeTable
extends Action<MigrateDeltaLakeTable, MigrateDeltaLakeTable.Result> {

MigrateDeltaLakeTable tableProperties(Map<String, String> properties);
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated

interface Result {

/** Returns the number of imported data files. */
long importedDataFilesCount();
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated
}
}
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ plugins {
id 'nebula.dependency-recommender' version '11.0.0'
}

String scalaVersion = System.getProperty("scalaVersion") != null ? System.getProperty("scalaVersion") : System.getProperty("defaultScalaVersion")

try {
// apply these plugins in a try-catch block so that we can handle cases without .git directory
apply plugin: 'com.palantir.git-version'
Expand Down Expand Up @@ -294,6 +296,8 @@ project(':iceberg-core') {
exclude group: 'org.slf4j', module: 'slf4j-log4j12'
}

compileOnly "io.delta:delta-standalone_${scalaVersion}"

testImplementation "org.eclipse.jetty:jetty-servlet"
testImplementation "org.eclipse.jetty:jetty-server"
testImplementation 'org.mock-server:mockserver-netty'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/*
* 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.iceberg;

import io.delta.standalone.DeltaLog;
import io.delta.standalone.VersionLog;
import io.delta.standalone.actions.Action;
import io.delta.standalone.actions.AddFile;
import io.delta.standalone.actions.RemoveFile;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.hadoop.conf.Configuration;
import org.apache.iceberg.actions.BaseMigrateDeltaLakeTableActionResult;
import org.apache.iceberg.actions.MigrateDeltaLakeTable;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class BaseMigrateDeltaLakeTableAction implements MigrateDeltaLakeTable {

private static final Logger LOG = LoggerFactory.getLogger(BaseMigrateDeltaLakeTableAction.class);
private final String parquetPostfix = ".parquet";
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated
private final String avroPostfix = ".avro";
private final String orcPostfix = ".orc";
private final Map<String, String> additionalProperties = Maps.newHashMap();
private final DeltaLog deltaLog;
private final Catalog icebergCatalog;
private final String deltaTableLocation;
private final TableIdentifier newTableIdentifier;
private final Configuration hadoopConfiguration;

public BaseMigrateDeltaLakeTableAction(
Catalog icebergCatalog,
String deltaTableLocation,
TableIdentifier newTableIdentifier,
Configuration hadoopConfiguration) {
this.icebergCatalog = icebergCatalog;
this.deltaTableLocation = deltaTableLocation;
this.newTableIdentifier = newTableIdentifier;
this.hadoopConfiguration = hadoopConfiguration;
// TODO: check whether we can retrieve hadoopConfiguration directly
this.deltaLog = DeltaLog.forTable(this.hadoopConfiguration, this.deltaTableLocation);
}

@Override
public MigrateDeltaLakeTable tableProperties(Map<String, String> properties) {
additionalProperties.putAll(properties);
return this;
}

@Override
public Result execute() {
io.delta.standalone.Snapshot updatedSnapshot = deltaLog.update();
Schema schema = convertDeltaLakeSchemaToSchema(updatedSnapshot.getMetadata().getSchema());
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated
PartitionSpec partitionSpec = getPartitionSpecFromDeltaSnapshot(schema);
// TODO: check whether we need more info when initializing the table
Table icebergTable =
this.icebergCatalog.createTable(
newTableIdentifier,
schema,
partitionSpec,
destTableProperties(
updatedSnapshot, this.deltaTableLocation, this.additionalProperties));

copyFromDeltaLakeToIceberg(icebergTable, partitionSpec);

Snapshot snapshot = icebergTable.currentSnapshot();
long totalDataFiles =
Long.parseLong(snapshot.summary().get(SnapshotSummary.TOTAL_DATA_FILES_PROP));
LOG.info(
"Successfully loaded Iceberg metadata for {} files to {}",
totalDataFiles,
deltaTableLocation);
return new BaseMigrateDeltaLakeTableActionResult(totalDataFiles);
}

/** TODO: check the correctness for nested schema */
private Schema convertDeltaLakeSchemaToSchema(io.delta.standalone.types.StructType deltaSchema) {
Type converted =
DeltaLakeDataTypeVisitor.visit(deltaSchema, new DeltaLakeTypeToType(deltaSchema));
return new Schema(converted.asNestedType().asStructType().fields());
}

private PartitionSpec getPartitionSpecFromDeltaSnapshot(Schema schema) {
List<String> partitionNames = deltaLog.snapshot().getMetadata().getPartitionColumns();
if (partitionNames.isEmpty()) {
return PartitionSpec.unpartitioned();
}

PartitionSpec.Builder builder = PartitionSpec.builderFor(schema);
for (String partitionName : partitionNames) {
builder.identity(partitionName);
}
return builder.build();
}

private void copyFromDeltaLakeToIceberg(Table table, PartitionSpec spec) {
// TODO: double check the arguments' meaning here
Iterator<VersionLog> it = deltaLog.getChanges(0, false);
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated

while (it.hasNext()) {
VersionLog versionLog = it.next();
List<Action> actions = versionLog.getActions();

// We first need to iterate through to see what kind of transaction this was. There are 3
// cases:
// 1. AppendFile - when there are only AddFile instances (an INSERT on the table)
// 2. DeleteFiles - when there are only RemoveFile instances (a DELETE where all the records
// of file(s) were removed
// 3. OverwriteFiles - when there are a mix of AddFile and RemoveFile (a DELETE/UPDATE)

// Create a map of Delta Lake Action (AddFile, RemoveFile, etc.) --> List<Action>
Map<String, List<Action>> deltaLakeActionMap =
actions.stream()
.filter(action -> action instanceof AddFile || action instanceof RemoveFile)
.collect(Collectors.groupingBy(a -> a.getClass().getSimpleName()));

// Scan the map so that we know what type of transaction this will be in Iceberg
IcebergTransactionType icebergTransactionType =
getIcebergTransactionTypeFromDeltaActions(deltaLakeActionMap);
if (icebergTransactionType == null) {
// TODO: my understanding here is that if the transaction type is undefined,
// we can no longer continue even the next versionLog contains valid transaction type
// may need further check
return;
}

List<DataFile> filesToAdd = Lists.newArrayList();
List<DataFile> filesToRemove = Lists.newArrayList();
for (Action action : Iterables.concat(deltaLakeActionMap.values())) {
DataFile dataFile = buildDataFileFromAction(action, table, spec);
if (action instanceof AddFile) {
filesToAdd.add(dataFile);
} else if (action instanceof RemoveFile) {
filesToRemove.add(dataFile);
} else {
// TODO: refactor this exception
throw new RuntimeException("Wrong action type");
}
}

switch (icebergTransactionType) {
case APPEND_FILES:
AppendFiles appendFiles = table.newAppend();
filesToAdd.forEach(appendFiles::appendFile);
appendFiles.commit();
break;
case DELETE_FILES:
DeleteFiles deleteFiles = table.newDelete();
filesToRemove.forEach(deleteFiles::deleteFile);
deleteFiles.commit();
break;
case OVERWRITE_FILES:
OverwriteFiles overwriteFiles = table.newOverwrite();
filesToAdd.forEach(overwriteFiles::addFile);
filesToRemove.forEach(overwriteFiles::deleteFile);
overwriteFiles.commit();
break;
}
}
}

@Nullable
private IcebergTransactionType getIcebergTransactionTypeFromDeltaActions(
Map<String, List<Action>> actionsMap) {
IcebergTransactionType icebergTransactionType;
if (actionsMap.containsKey(AddFile.class.getSimpleName())
&& !actionsMap.containsKey(RemoveFile.class.getSimpleName())) {
icebergTransactionType = IcebergTransactionType.APPEND_FILES;
} else if (actionsMap.containsKey(RemoveFile.class.getSimpleName())
&& !actionsMap.containsKey(AddFile.class.getSimpleName())) {
icebergTransactionType = IcebergTransactionType.DELETE_FILES;
} else if (actionsMap.containsKey(AddFile.class.getSimpleName())
&& actionsMap.containsKey(RemoveFile.class.getSimpleName())) {
icebergTransactionType = IcebergTransactionType.OVERWRITE_FILES;
} else {
// Some other type of transaction, we can ignore
return null;
}
return icebergTransactionType;
}

private DataFile buildDataFileFromAction(Action action, Table table, PartitionSpec spec) {
String path;
long size;
Map<String, String> partitionValues;

if (action instanceof AddFile) {
AddFile addFile = (AddFile) action;
path = addFile.getPath();
size = addFile.getSize();
partitionValues = addFile.getPartitionValues();
} else if (action instanceof RemoveFile) {
RemoveFile removeFile = (RemoveFile) action;
path = removeFile.getPath();
size =
removeFile
.getSize()
.orElseThrow(
() ->
new RuntimeException(
String.format("File %s removed with specifying a size", path)));
partitionValues =
Optional.ofNullable(removeFile.getPartitionValues())
.orElseThrow(
() ->
new RuntimeException(
String.format(
"File %s removed without specifying partition values", path)));
} else {
throw new IllegalStateException(
String.format(
"Unexpected action type for Delta Lake: %s", action.getClass().getSimpleName()));
}

String fullFilePath = deltaLog.getPath().toString() + File.separator + path;
FileFormat format = determineFileFormatFromPath(fullFilePath);

Metrics metrics = getMetricsForFile(table, fullFilePath, format);
String partition =
spec.fields().stream()
.map(PartitionField::name)
.map(name -> String.format("%s=%s", name, partitionValues.get(name)))
.collect(Collectors.joining("/"));

return DataFiles.builder(spec)
.withPath(fullFilePath)
.withFormat(format)
.withFileSizeInBytes(size)
.withMetrics(metrics)
.withPartitionPath(partition)
.withRecordCount(metrics.recordCount())
.build();
}

protected abstract Metrics getMetricsForFile(Table table, String fullFilePath, FileFormat format);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Based on my understanding, we must access the relevant file's package (e.g. iceberg-parquet) to get the metrics. Hence, I have to make this method abstract since this class is currently in iceberg-core. I feel like we may want to implement this in some other ways: (e.g. make this class more abstract, add a new package called iceberg-migration,etc). Please correct me if I misunderstand something about processing data files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

agree, I was proposing iceberg-deltalake, because for example if we can also later have iceberg-hudi, and people can just take 1 dependency for their migration purpose, instead of multiple.


private FileFormat determineFileFormatFromPath(String path) {
if (path.endsWith(parquetPostfix)) {
return FileFormat.PARQUET;
} else if (path.endsWith(avroPostfix)) {
return FileFormat.AVRO;
} else if (path.endsWith(orcPostfix)) {
return FileFormat.ORC;
} else {
throw new RuntimeException("The format of the file is unsupported: " + path);
}
}

private static Map<String, String> destTableProperties(
io.delta.standalone.Snapshot deltaSnapshot,
String tableLocation,
Map<String, String> additionalProperties) {
Map<String, String> properties = Maps.newHashMap();

properties.putAll(deltaSnapshot.getMetadata().getConfiguration());
properties.putAll(
ImmutableMap.of(
"provider",
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated
"iceberg",
"migrated",
Comment thread
JonasJ-ap marked this conversation as resolved.
Outdated
"true",
"table_type",
"iceberg",
"location",
tableLocation));
properties.putAll(additionalProperties);

return properties;
}

private enum IcebergTransactionType {
APPEND_FILES,
DELETE_FILES,
OVERWRITE_FILES
}
}
Loading