-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Implement Iceberg OPTIMIZE #10497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Iceberg OPTIMIZE #10497
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,12 +22,17 @@ | |
| import com.google.common.collect.Iterables; | ||
| import io.airlift.json.JsonCodec; | ||
| import io.airlift.slice.Slice; | ||
| import io.airlift.units.DataSize; | ||
| import io.trino.plugin.base.classloader.ClassLoaderSafeSystemTable; | ||
| import io.trino.plugin.hive.HiveApplyProjectionUtil; | ||
| import io.trino.plugin.hive.HiveApplyProjectionUtil.ProjectedColumnRepresentation; | ||
| import io.trino.plugin.hive.HiveWrittenPartitions; | ||
| import io.trino.plugin.iceberg.procedure.IcebergOptimizeHandle; | ||
| import io.trino.plugin.iceberg.procedure.IcebergTableExecuteHandle; | ||
| import io.trino.plugin.iceberg.procedure.IcebergTableProcedureId; | ||
| import io.trino.spi.TrinoException; | ||
| import io.trino.spi.connector.Assignment; | ||
| import io.trino.spi.connector.BeginTableExecuteResult; | ||
| import io.trino.spi.connector.CatalogSchemaName; | ||
| import io.trino.spi.connector.CatalogSchemaTableName; | ||
| import io.trino.spi.connector.ColumnHandle; | ||
|
|
@@ -39,6 +44,7 @@ | |
| import io.trino.spi.connector.ConnectorOutputMetadata; | ||
| import io.trino.spi.connector.ConnectorOutputTableHandle; | ||
| import io.trino.spi.connector.ConnectorSession; | ||
| import io.trino.spi.connector.ConnectorTableExecuteHandle; | ||
| import io.trino.spi.connector.ConnectorTableHandle; | ||
| import io.trino.spi.connector.ConnectorTableMetadata; | ||
| import io.trino.spi.connector.ConnectorTableProperties; | ||
|
|
@@ -63,11 +69,13 @@ | |
| import io.trino.spi.statistics.TableStatistics; | ||
| import io.trino.spi.type.TypeManager; | ||
| import org.apache.iceberg.AppendFiles; | ||
| import org.apache.iceberg.DataFile; | ||
| import org.apache.iceberg.DataFiles; | ||
| import org.apache.iceberg.FileScanTask; | ||
| import org.apache.iceberg.PartitionField; | ||
| import org.apache.iceberg.PartitionSpec; | ||
| import org.apache.iceberg.PartitionSpecParser; | ||
| import org.apache.iceberg.RewriteFiles; | ||
| import org.apache.iceberg.Schema; | ||
| import org.apache.iceberg.SchemaParser; | ||
| import org.apache.iceberg.Snapshot; | ||
|
|
@@ -83,6 +91,7 @@ | |
| import java.util.Collection; | ||
| import java.util.Comparator; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
@@ -121,6 +130,7 @@ | |
| import static io.trino.plugin.iceberg.TrinoHiveCatalog.DEPENDS_ON_TABLES; | ||
| import static io.trino.plugin.iceberg.TypeConverter.toIcebergType; | ||
| import static io.trino.plugin.iceberg.TypeConverter.toTrinoType; | ||
| import static io.trino.plugin.iceberg.procedure.IcebergTableProcedureId.OPTIMIZE; | ||
| import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; | ||
| import static io.trino.spi.type.BigintType.BIGINT; | ||
| import static java.util.Collections.singletonList; | ||
|
|
@@ -545,6 +555,158 @@ public ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, Connect | |
| return new IcebergColumnHandle(primitiveColumnIdentity(0, "$row_id"), BIGINT, ImmutableList.of(), BIGINT, Optional.empty()); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<ConnectorTableExecuteHandle> getTableHandleForExecute( | ||
| ConnectorSession session, | ||
| ConnectorTableHandle connectorTableHandle, | ||
| String procedureName, | ||
| Map<String, Object> executeProperties) | ||
| { | ||
| IcebergTableHandle tableHandle = (IcebergTableHandle) connectorTableHandle; | ||
|
|
||
| IcebergTableProcedureId procedureId; | ||
| try { | ||
| procedureId = IcebergTableProcedureId.valueOf(procedureName); | ||
| } | ||
| catch (IllegalArgumentException e) { | ||
| throw new IllegalArgumentException("Unknown procedure '" + procedureName + "'"); | ||
| } | ||
|
|
||
| switch (procedureId) { | ||
| case OPTIMIZE: | ||
| return getTableHandleForOptimize(session, tableHandle, executeProperties); | ||
| } | ||
|
|
||
| throw new IllegalArgumentException("Unknown procedure: " + procedureId); | ||
| } | ||
|
|
||
| private Optional<ConnectorTableExecuteHandle> getTableHandleForOptimize(ConnectorSession session, IcebergTableHandle tableHandle, Map<String, Object> executeProperties) | ||
| { | ||
| DataSize maxScannedFileSize = (DataSize) executeProperties.get("file_size_threshold"); | ||
| Table icebergTable = catalog.loadTable(session, tableHandle.getSchemaTableName()); | ||
|
|
||
| return Optional.of(new IcebergTableExecuteHandle( | ||
| tableHandle.getSchemaTableName(), | ||
| OPTIMIZE, | ||
| new IcebergOptimizeHandle( | ||
| SchemaParser.toJson(icebergTable.schema()), | ||
| PartitionSpecParser.toJson(icebergTable.spec()), | ||
| getColumns(icebergTable.schema(), typeManager), | ||
| getFileFormat(icebergTable), | ||
| icebergTable.properties(), | ||
| maxScannedFileSize), | ||
| icebergTable.location())); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<ConnectorNewTableLayout> getLayoutForTableExecute(ConnectorSession session, ConnectorTableExecuteHandle tableExecuteHandle) | ||
| { | ||
| IcebergTableExecuteHandle executeHandle = (IcebergTableExecuteHandle) tableExecuteHandle; | ||
| switch (executeHandle.getProcedureId()) { | ||
| case OPTIMIZE: | ||
| return getLayoutForOptimize(session, executeHandle); | ||
| } | ||
| throw new IllegalArgumentException("Unknown procedure '" + executeHandle.getProcedureId() + "'"); | ||
| } | ||
|
|
||
| private Optional<ConnectorNewTableLayout> getLayoutForOptimize(ConnectorSession session, IcebergTableExecuteHandle executeHandle) | ||
| { | ||
| Table icebergTable = catalog.loadTable(session, executeHandle.getSchemaTableName()); | ||
| return getWriteLayout(icebergTable.schema(), icebergTable.spec()); | ||
| } | ||
|
|
||
| @Override | ||
| public BeginTableExecuteResult<ConnectorTableExecuteHandle, ConnectorTableHandle> beginTableExecute( | ||
| ConnectorSession session, | ||
| ConnectorTableExecuteHandle tableExecuteHandle, | ||
| ConnectorTableHandle updatedSourceTableHandle) | ||
| { | ||
| IcebergTableExecuteHandle executeHandle = (IcebergTableExecuteHandle) tableExecuteHandle; | ||
| IcebergTableHandle table = (IcebergTableHandle) updatedSourceTableHandle; | ||
| switch (executeHandle.getProcedureId()) { | ||
| case OPTIMIZE: | ||
| return beginOptimize(session, executeHandle, table); | ||
| } | ||
| throw new IllegalArgumentException("Unknown procedure '" + executeHandle.getProcedureId() + "'"); | ||
| } | ||
|
|
||
| private BeginTableExecuteResult<ConnectorTableExecuteHandle, ConnectorTableHandle> beginOptimize( | ||
| ConnectorSession session, | ||
| IcebergTableExecuteHandle executeHandle, | ||
| IcebergTableHandle table) | ||
| { | ||
| IcebergOptimizeHandle optimizeHandle = (IcebergOptimizeHandle) executeHandle.getProcedureHandle(); | ||
| Table icebergTable = catalog.loadTable(session, table.getSchemaTableName()); | ||
|
|
||
| verify(transaction == null, "transaction already set"); | ||
| transaction = icebergTable.newTransaction(); | ||
|
|
||
| return new BeginTableExecuteResult<>( | ||
| executeHandle, | ||
| table.forOptimize(true, optimizeHandle.getMaxScannedFileSize())); | ||
| } | ||
|
|
||
| @Override | ||
| public void finishTableExecute(ConnectorSession session, ConnectorTableExecuteHandle tableExecuteHandle, Collection<Slice> fragments, List<Object> splitSourceInfo) | ||
| { | ||
| IcebergTableExecuteHandle executeHandle = (IcebergTableExecuteHandle) tableExecuteHandle; | ||
| switch (executeHandle.getProcedureId()) { | ||
| case OPTIMIZE: | ||
| finishOptimize(executeHandle, fragments, splitSourceInfo); | ||
| return; | ||
| } | ||
| throw new IllegalArgumentException("Unknown procedure '" + executeHandle.getProcedureId() + "'"); | ||
| } | ||
|
|
||
| private void finishOptimize(IcebergTableExecuteHandle executeHandle, Collection<Slice> fragments, List<Object> splitSourceInfo) | ||
| { | ||
| IcebergOptimizeHandle optimizeHandle = (IcebergOptimizeHandle) executeHandle.getProcedureHandle(); | ||
| Table icebergTable = transaction.table(); | ||
|
|
||
| // paths to be deleted | ||
| Set<DataFile> scannedFiles = splitSourceInfo.stream() | ||
| .map(DataFile.class::cast) | ||
| .collect(toImmutableSet()); | ||
|
|
||
| List<CommitTaskData> commitTasks = fragments.stream() | ||
| .map(slice -> commitTaskCodec.fromJson(slice.getBytes())) | ||
| .collect(toImmutableList()); | ||
|
|
||
| Type[] partitionColumnTypes = icebergTable.spec().fields().stream() | ||
| .map(field -> field.transform().getResultType( | ||
| icebergTable.schema().findType(field.sourceId()))) | ||
| .toArray(Type[]::new); | ||
|
|
||
| Set<DataFile> newFiles = new HashSet<>(); | ||
| for (CommitTaskData task : commitTasks) { | ||
| DataFiles.Builder builder = DataFiles.builder(icebergTable.spec()) | ||
| .withPath(task.getPath()) | ||
| .withFileSizeInBytes(task.getFileSizeInBytes()) | ||
| .withFormat(optimizeHandle.getFileFormat()) | ||
| .withMetrics(task.getMetrics().metrics()); | ||
|
|
||
| if (!icebergTable.spec().fields().isEmpty()) { | ||
| String partitionDataJson = task.getPartitionDataJson() | ||
| .orElseThrow(() -> new VerifyException("No partition data for partitioned table")); | ||
| builder.withPartition(PartitionData.fromJson(partitionDataJson, partitionColumnTypes)); | ||
| } | ||
|
|
||
| newFiles.add(builder.build()); | ||
| } | ||
|
|
||
| if (scannedFiles.isEmpty() && newFiles.isEmpty()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. assert we should not ever get one empty and other not? Feels like a bug situation.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scanned file list may be non empty, but resulting data may be empty, if input files were empty. |
||
| // Table scan turned out to be empty, nothing to commit | ||
| transaction = null; | ||
| return; | ||
| } | ||
|
|
||
| RewriteFiles rewriteFiles = transaction.newRewrite(); | ||
| rewriteFiles.rewriteFiles(scannedFiles, newFiles); | ||
| rewriteFiles.commit(); | ||
| transaction.commitTransaction(); | ||
| transaction = null; | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<Object> getInfo(ConnectorTableHandle tableHandle) | ||
| { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.