vinishjail97 commented on code in PR #935: URL: https://github.com/apache/incubator-xtable/pull/935#discussion_r4225427773
########## xtable-core/src/main/java/org/apache/xtable/index/HudiBackedIcebergSecondaryIndex.java: ########## @@ -0,0 +1,243 @@ +/* + * 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.xtable.index; + +import static org.apache.hudi.metadata.HoodieTableMetadataUtil.PARTITION_NAME_SECONDARY_INDEX_PREFIX; +import static org.apache.spark.sql.functions.col; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import lombok.extern.log4j.Log4j2; + +import org.apache.hadoop.conf.Configuration; +import org.apache.spark.api.java.JavaPairRDD; +import org.apache.spark.api.java.JavaRDD; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoders; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructType; + +import org.apache.hudi.client.common.HoodieSparkEngineContext; +import org.apache.hudi.common.config.HoodieMetadataConfig; +import org.apache.hudi.common.data.HoodiePairData; +import org.apache.hudi.common.table.HoodieTableMetaClient; +import org.apache.hudi.common.table.HoodieTableVersion; +import org.apache.hudi.data.HoodieJavaPairRDD; +import org.apache.hudi.data.HoodieJavaRDD; +import org.apache.hudi.metadata.HoodieBackedTableMetadata; +import org.apache.hudi.metadata.HoodieTableMetadataUtil; +import org.apache.hudi.storage.HoodieStorage; +import org.apache.hudi.storage.hadoop.HoodieHadoopStorage; + +import org.apache.iceberg.Table; + +import scala.Tuple2; + +import com.google.common.base.Preconditions; + +import org.apache.xtable.catalog.TableFormatUtils; +import org.apache.xtable.conversion.ConversionConfig; +import org.apache.xtable.conversion.ConversionController; +import org.apache.xtable.conversion.SourceTable; +import org.apache.xtable.conversion.TargetTable; +import org.apache.xtable.hudi.HudiTargetConfig; +import org.apache.xtable.iceberg.IcebergConversionSourceProvider; +import org.apache.xtable.model.storage.TableFormat; +import org.apache.xtable.model.sync.SyncResult; +import org.apache.xtable.model.sync.SyncStatusCode; + +/** + * Secondary index for an Iceberg table backed by the Hudi metadata table. The index is built by + * syncing the Iceberg table to Hudi with the record index and a secondary index enabled; the Hudi + * metadata lives under {@code <data location>/.hoodie/metadata}. Because the Iceberg data files + * carry no Hudi record keys, the record key of a row is {@code <file path relative to the data + * location>_<row position>}, which a lookup resolves back to a file path and row position. + */ +@Log4j2 +public class HudiBackedIcebergSecondaryIndex implements Index<Table> { + private final String tableLocation; + private final String dataPath; + private final SourceTable sourceTable; + private final Properties targetTableProperties; + private final SparkSession sparkSession; + private final JavaSparkContext javaSparkContext; + private final HoodieSparkEngineContext engineContext; + + /** + * @param icebergTable the table to index + * @param sourceTable how XTable loads the table for a sync, because a loaded table does not + * expose the catalog it came from. A table of a catalog sets its catalog config, name and + * namespace, and a table without a catalog config is loaded from its location + * @param sparkSession the Spark session used to build and query the index + * @param targetTableProperties additional {@link HudiTargetConfig} properties for the sync, for + * example the record index file group counts + */ + public HudiBackedIcebergSecondaryIndex( + Table icebergTable, + SourceTable sourceTable, + SparkSession sparkSession, + Properties targetTableProperties) { + Preconditions.checkArgument( + TableFormat.ICEBERG.equals(sourceTable.getFormatName()), + "Expected an Iceberg source table but got format %s", + sourceTable.getFormatName()); + this.tableLocation = icebergTable.location(); + this.dataPath = + TableFormatUtils.getTableDataLocation( + TableFormat.ICEBERG, tableLocation, icebergTable.properties()); + // the Hudi table lives under the data location, which Iceberg resolves from the table + // properties + this.sourceTable = sourceTable.toBuilder().dataPath(dataPath).build(); + this.targetTableProperties = targetTableProperties; + this.sparkSession = sparkSession; + this.javaSparkContext = JavaSparkContext.fromSparkContext(sparkSession.sparkContext()); + this.engineContext = new HoodieSparkEngineContext(javaSparkContext); + } + + @Override + public boolean doesIndexExist(String columnName) { + return HoodieTableMetadataUtil.metadataPartitionExists( + dataPath, engineContext, PARTITION_NAME_SECONDARY_INDEX_PREFIX + columnName); + } + + @Override + public void syncIndex(Table icebergTable, String columnName) { + Configuration configuration = javaSparkContext.hadoopConfiguration(); + IcebergConversionSourceProvider sourceProvider = new IcebergConversionSourceProvider(); + sourceProvider.init(configuration); + // the sync reports a failed target in its result instead of throwing + SyncResult syncResult = + new ConversionController(configuration) + .sync(getConversionConfig(icebergTable, columnName), sourceProvider) + .get(TableFormat.HUDI); + if (syncResult == null Review Comment: [P2] Repeated sync fails when the table has no changes Self-review finding: after a successful initial sync, IcebergConversionSource.getCommitsBacklog returns an empty backlog when no new snapshot exists. TableFormatSync.syncChanges and ConversionController then return an empty result map. The null check throws even though the index is current. Reproduced on dee82e40 by inserting 20 rows and calling syncIndex(table, "id") twice without changing the table. The second call throws IllegalStateException with `no sync result for the Hudi target`. Periodic syncs therefore fail whenever the source has no changes. Required follow-up: recognize the no-change case while preserving failure propagation for actual ERROR results, and add a repeated-sync regression test before merge. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
