|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, software |
| 13 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | + * See the License for the specific language governing permissions and |
| 16 | + * limitations under the License. |
| 17 | + */ |
| 18 | + |
| 19 | +package org.apache.paimon.spark.procedure; |
| 20 | + |
| 21 | +import org.apache.paimon.disk.IOManager; |
| 22 | +import org.apache.paimon.predicate.Predicate; |
| 23 | +import org.apache.paimon.reader.RecordReader; |
| 24 | +import org.apache.paimon.reader.RecordReaderIterator; |
| 25 | +import org.apache.paimon.spark.SparkTable; |
| 26 | +import org.apache.paimon.spark.SparkUtils; |
| 27 | +import org.apache.paimon.spark.catalyst.analysis.expressions.ExpressionUtils; |
| 28 | +import org.apache.paimon.table.ChainGroupReadTable; |
| 29 | +import org.apache.paimon.table.FallbackReadFileStoreTable; |
| 30 | +import org.apache.paimon.table.FileStoreTable; |
| 31 | +import org.apache.paimon.table.Table; |
| 32 | +import org.apache.paimon.table.sink.BatchTableCommit; |
| 33 | +import org.apache.paimon.table.sink.BatchTableWrite; |
| 34 | +import org.apache.paimon.table.sink.BatchWriteBuilder; |
| 35 | +import org.apache.paimon.table.sink.CommitMessage; |
| 36 | +import org.apache.paimon.table.sink.CommitMessageSerializer; |
| 37 | +import org.apache.paimon.table.source.ChainSplit; |
| 38 | +import org.apache.paimon.table.source.Split; |
| 39 | +import org.apache.paimon.table.source.TableRead; |
| 40 | +import org.apache.paimon.utils.InternalRowPartitionComputer; |
| 41 | +import org.apache.paimon.utils.ParameterUtils; |
| 42 | +import org.apache.paimon.utils.Preconditions; |
| 43 | +import org.apache.paimon.utils.StringUtils; |
| 44 | + |
| 45 | +import org.apache.spark.api.java.JavaRDD; |
| 46 | +import org.apache.spark.api.java.JavaSparkContext; |
| 47 | +import org.apache.spark.api.java.function.FlatMapFunction; |
| 48 | +import org.apache.spark.sql.catalyst.InternalRow; |
| 49 | +import org.apache.spark.sql.catalyst.expressions.Expression; |
| 50 | +import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan; |
| 51 | +import org.apache.spark.sql.connector.catalog.Identifier; |
| 52 | +import org.apache.spark.sql.connector.catalog.TableCatalog; |
| 53 | +import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation; |
| 54 | +import org.apache.spark.sql.types.DataTypes; |
| 55 | +import org.apache.spark.sql.types.Metadata; |
| 56 | +import org.apache.spark.sql.types.StructField; |
| 57 | +import org.apache.spark.sql.types.StructType; |
| 58 | +import org.slf4j.Logger; |
| 59 | +import org.slf4j.LoggerFactory; |
| 60 | + |
| 61 | +import java.util.ArrayList; |
| 62 | +import java.util.Iterator; |
| 63 | +import java.util.List; |
| 64 | +import java.util.Map; |
| 65 | + |
| 66 | +import scala.Option; |
| 67 | + |
| 68 | +import static org.apache.paimon.spark.utils.SparkProcedureUtils.toWhere; |
| 69 | +import static org.apache.paimon.utils.Preconditions.checkArgument; |
| 70 | +import static org.apache.spark.sql.types.DataTypes.StringType; |
| 71 | + |
| 72 | +/** |
| 73 | + * Chain merge procedure. Usage: |
| 74 | + * |
| 75 | + * <pre><code> |
| 76 | + * CALL sys.compact(table => 'tableId', partitions => 'p1=0,p2=0', target_branch => 'snapshot') |
| 77 | + * </code></pre> |
| 78 | + */ |
| 79 | +public class ChainMergeProcedure extends BaseProcedure { |
| 80 | + |
| 81 | + private static final Logger LOG = LoggerFactory.getLogger(ChainMergeProcedure.class); |
| 82 | + |
| 83 | + private static final ProcedureParameter[] PARAMETERS = |
| 84 | + new ProcedureParameter[] { |
| 85 | + ProcedureParameter.required("table", StringType), |
| 86 | + ProcedureParameter.required("partitions", StringType), |
| 87 | + ProcedureParameter.optional("target_branch", StringType), |
| 88 | + }; |
| 89 | + |
| 90 | + private static final StructType OUTPUT_TYPE = |
| 91 | + new StructType( |
| 92 | + new StructField[] { |
| 93 | + new StructField("result", DataTypes.BooleanType, true, Metadata.empty()) |
| 94 | + }); |
| 95 | + |
| 96 | + protected ChainMergeProcedure(TableCatalog tableCatalog) { |
| 97 | + super(tableCatalog); |
| 98 | + } |
| 99 | + |
| 100 | + @Override |
| 101 | + public ProcedureParameter[] parameters() { |
| 102 | + return PARAMETERS; |
| 103 | + } |
| 104 | + |
| 105 | + @Override |
| 106 | + public StructType outputType() { |
| 107 | + return OUTPUT_TYPE; |
| 108 | + } |
| 109 | + |
| 110 | + private boolean blank(InternalRow args, int index) { |
| 111 | + return args.isNullAt(index) || StringUtils.isNullOrWhitespaceOnly(args.getString(index)); |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + public InternalRow[] call(InternalRow args) { |
| 116 | + Identifier tableIdent = toIdentifier(args.getString(0), PARAMETERS[0].name()); |
| 117 | + String partitions = args.getString(1); |
| 118 | + SparkTable sparkTable = loadSparkTable(tableIdent); |
| 119 | + String targetBranch = |
| 120 | + blank(args, 2) |
| 121 | + ? sparkTable.coreOptions().scanFallbackSnapshotBranch() |
| 122 | + : args.getString(2); |
| 123 | + List<Map<String, String>> compactPartitions = |
| 124 | + ParameterUtils.getPartitions(partitions.split(";")); |
| 125 | + validataChainMerge(sparkTable, targetBranch, partitions, compactPartitions); |
| 126 | + DataSourceV2Relation relation = createRelation(tableIdent); |
| 127 | + Expression condition = |
| 128 | + getPartitionCondition(relation, sparkTable.table(), toWhere(partitions)); |
| 129 | + boolean executed = |
| 130 | + executeChainMerge( |
| 131 | + (FallbackReadFileStoreTable) sparkTable.getTable(), |
| 132 | + condition, |
| 133 | + relation, |
| 134 | + targetBranch); |
| 135 | + return new InternalRow[] {newInternalRow(executed)}; |
| 136 | + } |
| 137 | + |
| 138 | + public boolean executeChainMerge( |
| 139 | + FallbackReadFileStoreTable chainTable, |
| 140 | + Expression partCondition, |
| 141 | + DataSourceV2Relation relation, |
| 142 | + String targetBranch) { |
| 143 | + |
| 144 | + // build scan for the specific partition |
| 145 | + Preconditions.checkArgument( |
| 146 | + chainTable.other() instanceof ChainGroupReadTable, |
| 147 | + "The chain merge should perform on the ChainFileStoreTable"); |
| 148 | + |
| 149 | + Option<Predicate> filter = |
| 150 | + ExpressionUtils.convertConditionToPaimonPredicate( |
| 151 | + partCondition, |
| 152 | + ((LogicalPlan) relation).output(), |
| 153 | + chainTable.rowType(), |
| 154 | + false); |
| 155 | + |
| 156 | + ChainGroupReadTable chainGroupReadTable = (ChainGroupReadTable) chainTable.other(); |
| 157 | + ChainGroupReadTable.ChainTableBatchScan scan = |
| 158 | + (ChainGroupReadTable.ChainTableBatchScan) chainGroupReadTable.newScan(); |
| 159 | + if (filter.isDefined()) { |
| 160 | + scan.withFilter(filter.get()); |
| 161 | + } |
| 162 | + List<Split> splits = scan.plan().splits(); |
| 163 | + |
| 164 | + if (splits.isEmpty()) { |
| 165 | + LOG.info("The target partition={} is empty", partCondition); |
| 166 | + return false; |
| 167 | + } |
| 168 | + Preconditions.checkArgument( |
| 169 | + splits.stream().allMatch(s -> (s instanceof ChainSplit)), |
| 170 | + "The chain merge only accepts ChainDataSplit"); |
| 171 | + |
| 172 | + // build snapshot branch write builder with static partition overwrite |
| 173 | + FileStoreTable targetTable = ((ChainGroupReadTable) chainTable.other()).wrapped(); |
| 174 | + checkArgument( |
| 175 | + targetBranch.equals(targetTable.coreOptions().branch()), |
| 176 | + "chain_merge should merge to snapshot branch"); |
| 177 | + InternalRowPartitionComputer computer = |
| 178 | + new InternalRowPartitionComputer( |
| 179 | + chainTable.coreOptions().partitionDefaultName(), |
| 180 | + chainTable.schema().logicalPartitionType(), |
| 181 | + chainTable.schema().partitionKeys().toArray(new String[0]), |
| 182 | + chainTable.coreOptions().legacyPartitionName()); |
| 183 | + Map<String, String> targetPartition = |
| 184 | + computer.generatePartValues(((ChainSplit) splits.get(0)).logicalPartition()); |
| 185 | + |
| 186 | + LOG.info( |
| 187 | + "Direct chain_merge plan built, splits: {}, target partition: {}, target branch: {}", |
| 188 | + splits.size(), |
| 189 | + targetPartition, |
| 190 | + targetBranch); |
| 191 | + BatchWriteBuilder writeBuilder = |
| 192 | + targetTable.newBatchWriteBuilder().withOverwrite(targetPartition); |
| 193 | + JavaSparkContext javaSparkContext = new JavaSparkContext(spark().sparkContext()); |
| 194 | + |
| 195 | + JavaRDD<byte[]> commitMessageJavaRDD = |
| 196 | + javaSparkContext |
| 197 | + .parallelize(splits) |
| 198 | + .mapPartitions( |
| 199 | + (FlatMapFunction<Iterator<Split>, byte[]>) |
| 200 | + splitIterator -> { |
| 201 | + List<byte[]> serializedMessages = new ArrayList<>(); |
| 202 | + IOManager ioManager = SparkUtils.createIOManager(); |
| 203 | + BatchTableWrite write = writeBuilder.newWrite(); |
| 204 | + write.withIOManager(ioManager); |
| 205 | + while (splitIterator.hasNext()) { |
| 206 | + Split split = splitIterator.next(); |
| 207 | + try { |
| 208 | + TableRead read = |
| 209 | + chainGroupReadTable |
| 210 | + .newRead() |
| 211 | + .withIOManager(ioManager); |
| 212 | + RecordReader<org.apache.paimon.data.InternalRow> |
| 213 | + reader = read.createReader(split); |
| 214 | + try (RecordReader< |
| 215 | + org.apache.paimon.data |
| 216 | + .InternalRow> |
| 217 | + rr = reader) { |
| 218 | + RecordReaderIterator< |
| 219 | + org.apache.paimon.data |
| 220 | + .InternalRow> |
| 221 | + it = new RecordReaderIterator<>(rr); |
| 222 | + org.apache.paimon.data.InternalRow row; |
| 223 | + while ((row = it.next()) != null) { |
| 224 | + write.write(row); |
| 225 | + } |
| 226 | + } |
| 227 | + CommitMessageSerializer serializer = |
| 228 | + new CommitMessageSerializer(); |
| 229 | + List<CommitMessage> messages = |
| 230 | + write.prepareCommit(); |
| 231 | + for (CommitMessage commitMessage : messages) { |
| 232 | + serializedMessages.add( |
| 233 | + serializer.serialize( |
| 234 | + commitMessage)); |
| 235 | + } |
| 236 | + } finally { |
| 237 | + write.close(); |
| 238 | + ioManager.close(); |
| 239 | + } |
| 240 | + } |
| 241 | + return serializedMessages.iterator(); |
| 242 | + }); |
| 243 | + |
| 244 | + try (BatchTableCommit commit = writeBuilder.newCommit()) { |
| 245 | + CommitMessageSerializer serializer = new CommitMessageSerializer(); |
| 246 | + List<byte[]> serializedMessages = commitMessageJavaRDD.collect(); |
| 247 | + List<CommitMessage> messages = new ArrayList<>(serializedMessages.size()); |
| 248 | + for (byte[] serializedMessage : serializedMessages) { |
| 249 | + messages.add(serializer.deserialize(serializer.getVersion(), serializedMessage)); |
| 250 | + } |
| 251 | + commit.commit(messages); |
| 252 | + } catch (Exception e) { |
| 253 | + throw new RuntimeException(e); |
| 254 | + } |
| 255 | + return true; |
| 256 | + } |
| 257 | + |
| 258 | + private Expression getPartitionCondition( |
| 259 | + DataSourceV2Relation relation, Table table, String where) { |
| 260 | + Expression condition = null; |
| 261 | + if (!StringUtils.isNullOrWhitespaceOnly(where)) { |
| 262 | + condition = ExpressionUtils.resolveFilter(spark(), relation, where); |
| 263 | + checkArgument( |
| 264 | + ExpressionUtils.isValidPredicate( |
| 265 | + spark(), condition, table.partitionKeys().toArray(new String[0])), |
| 266 | + "Only partition predicate is supported, your predicate is %s, but partition keys are %s", |
| 267 | + condition, |
| 268 | + table.partitionKeys()); |
| 269 | + } |
| 270 | + return condition; |
| 271 | + } |
| 272 | + |
| 273 | + public static ProcedureBuilder builder() { |
| 274 | + return new BaseProcedure.Builder<ChainMergeProcedure>() { |
| 275 | + @Override |
| 276 | + public ChainMergeProcedure doBuild() { |
| 277 | + return new ChainMergeProcedure(tableCatalog()); |
| 278 | + } |
| 279 | + }; |
| 280 | + } |
| 281 | + |
| 282 | + private void validataChainMerge( |
| 283 | + SparkTable sparkTable, |
| 284 | + String targetBranch, |
| 285 | + String partitions, |
| 286 | + List<Map<String, String>> compactPartitions) { |
| 287 | + checkArgument( |
| 288 | + sparkTable.coreOptions().isChainTable(), "chain_merge only supports chain table"); |
| 289 | + checkArgument( |
| 290 | + targetBranch.equals(sparkTable.coreOptions().scanFallbackSnapshotBranch()), |
| 291 | + "chain_merge should merge to snapshot branch"); |
| 292 | + checkArgument( |
| 293 | + sparkTable.getTable() instanceof FallbackReadFileStoreTable, |
| 294 | + "The chain merge should perform on the chain table"); |
| 295 | + checkArgument( |
| 296 | + compactPartitions.size() == 1 |
| 297 | + && compactPartitions.get(0).size() |
| 298 | + == sparkTable.table().partitionKeys().size(), |
| 299 | + "chain_merge only supports one partition %s", |
| 300 | + partitions); |
| 301 | + } |
| 302 | +} |
0 commit comments