koodin9 commented on code in PR #18003: URL: https://github.com/apache/iceberg/pull/18003#discussion_r4357525943
########## kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/EqualityDeleteConverter.java: ########## @@ -0,0 +1,336 @@ +/* + * 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.connect.data; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.deletes.BaseDVFileWriter; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.DeleteWriteResult; +import org.apache.iceberg.io.OutputFileFactory; +import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; +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.TypeUtil; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.iceberg.util.StructLikeWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves equality delete files against the rows of a table and writes the matching row positions + * as deletion vectors, so the equality delete files never have to be committed. + * + * <p>An equality delete written by the sink deletes every row of an earlier commit whose identifier + * fields equal one of the deleted keys. The converter reads the keys, groups them by equality + * fields and partition, and asks a {@link KeyPositionResolver} for the rows that hold them. The + * default resolver, {@link ScanKeyPositionResolver}, scans the data files that the column + * statistics cannot rule out; an index-backed resolver can be plugged in instead. A data file that + * already has a deletion vector gets a merged one; the replaced vector is reported in {@link + * Result#rewrittenDvFiles()} and must be removed by the commit. + * + * <p>The positions are only valid for the snapshot they were resolved against, see {@link + * Result#baseSnapshotId()}. The commit that adds the deletion vectors has to validate that no + * conflicting data or delete files were added since then and re-run the conversion otherwise. + * {@link Result#conflictFilter()} narrows that validation to the deleted keys. + * + * <p>Requires format version 3. + */ +public class EqualityDeleteConverter { + + private static final Logger LOG = LoggerFactory.getLogger(EqualityDeleteConverter.class); + + private final Table table; + private final String branch; + private final KeyPositionResolver resolver; + private final DeleteLoader deleteLoader; + private final AtomicInteger scannedDataFiles = new AtomicInteger(); + + /** + * Creates a converter that finds the rows by scanning data files. + * + * @param table the table to resolve the deletes against + * @param branch the branch that is written, or null for the main branch + */ + public EqualityDeleteConverter(Table table, String branch) { + this(table, branch, new ScanKeyPositionResolver(table)); + } + + EqualityDeleteConverter(Table table, String branch, KeyPositionResolver resolver) { + this.table = table; + this.branch = branch; + this.resolver = resolver; + this.deleteLoader = new BaseDeleteLoader(deleteFile -> table.io().newInputFile(deleteFile)); + } + + /** + * Converts the given equality delete files into deletion vectors on the current snapshot of the + * branch. The table is expected to be refreshed by the caller. + */ + public Result convert(List<DeleteFile> eqDeleteFiles) { Review Comment: Done in 4f16f39c3. The constructor rejects tables below format version 3, so the check fails before anything is scanned or written. Format versions only go up, so a converter created for a v3 table stays valid. `testRejectsFormatVersion2` covers it. ########## kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/KeyPositionResolver.java: ########## @@ -0,0 +1,87 @@ +/* + * 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.connect.data; + +import java.io.IOException; +import java.util.Collection; +import java.util.function.LongConsumer; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.util.StructLikeSet; + +/** + * Finds the rows of a table snapshot whose identifier values are in a set of deleted keys. + * + * <p>{@link EqualityDeleteConverter} asks a resolver for the position of every row an equality + * delete would remove and writes those positions as deletion vectors. Implementations differ in how + * they locate the rows. The default {@link ScanKeyPositionResolver} reads the data files that the + * column statistics cannot rule out; an implementation backed by an index can look the keys up + * without touching data files. + */ +public interface KeyPositionResolver { Review Comment: It doesn't need to be. 7aa4314e8 makes it package-private and drops the Javadoc about plugging in another resolver. It can become public once a caller outside the package needs it. ########## kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/ScanKeyPositionResolver.java: ########## @@ -0,0 +1,319 @@ +/* + * 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.connect.data; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.LongConsumer; +import org.apache.iceberg.Accessor; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileScanTask; +import org.apache.iceberg.MetadataColumns; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.data.InternalRecordWrapper; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.formats.FormatModelRegistry; +import org.apache.iceberg.formats.ReadBuilder; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +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.Types; +import org.apache.iceberg.util.ContentFileUtil; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.iceberg.util.StructLikeWrapper; +import org.apache.iceberg.util.StructProjection; +import org.apache.iceberg.util.Tasks; +import org.apache.iceberg.util.ThreadPools; + +/** + * Resolves deleted keys by scanning data files. No state is kept between calls. + * + * <p>The candidate files are planned with the filters of {@link KeyFilters}: the partition of the + * equality delete files is pinned through its transform and the keys are matched with {@code IN} + * lists while they fit the comparison budget the table's data file count allows, with value ranges + * split at the largest gaps of the leading key column above that. Only the key columns and the row + * position of the candidate files are read, in parallel, and a row is matched by comparing its key + * with the deleted set. A cluster of hot keys and a few scattered keys therefore cost the files + * that hold them; keys spread evenly over a large table still match most files of the partition. + * + * <p>The filters only prune. A candidate is resolved only if the deletes apply to it under the scan + * planning rules: an unpartitioned delete applies to every data file, a partitioned delete only to + * data files with the same spec ID and partition values. + * + * <p>Data files that carry position delete files instead of deletion vectors are rejected, because + * a deletion vector replaces all position deletes of its data file. + */ +class ScanKeyPositionResolver implements KeyPositionResolver { + + private final Table table; + private final DeleteLoader deleteLoader; + + ScanKeyPositionResolver(Table table) { + this.table = table; + this.deleteLoader = new BaseDeleteLoader(deleteFile -> table.io().newInputFile(deleteFile)); + } + + @Override + public Resolution resolve( + Snapshot base, + Schema keySchema, + StructLikeSet keys, + PartitionSpec deleteSpec, + StructLike deletePartition) + throws IOException { + // the plan filter carries the partition pin and the key filter; only the key filter is passed + // to the file readers, whose row group filters evaluate column references, + // not partition transforms + Expression keyFilter = + KeyFilters.keyFilter(keySchema, keys, KeyFilters.comparisonsPerFile(base)); + Expression planFilter = + Expressions.and( + KeyFilters.partitionFilter(table.schema(), deleteSpec, deletePartition), keyFilter); + + // the partition filter only prunes; it also keeps files of other specs whose rows satisfy the + // transforms, so the spec and partition of every candidate are checked as well + PartitionMatcher partitionMatcher = new PartitionMatcher(deleteSpec, deletePartition); + List<FileScanTask> tasks = Lists.newArrayList(); + try (CloseableIterable<FileScanTask> planned = + table + .newScan() + .useSnapshot(base.snapshotId()) + .filter(planFilter) + .ignoreResiduals() + .planFiles()) { + for (FileScanTask task : planned) { + if (partitionMatcher.matches(task.spec(), task.file().partition())) { + tasks.add(task); + } + } + } + + Map<String, FileMatches> matches = Maps.newConcurrentMap(); + Tasks.foreach(tasks) + .executeWith(ThreadPools.getWorkerPool()) + .stopOnFailure() + .throwFailureWhenFinished() + .run(task -> match(task, keySchema, keys, keyFilter, matches), IOException.class); + + return new ScanResolution(matches.values(), tasks.size()); + } + + /** Reads the key columns and row positions of one data file and records the matching rows. */ + private void match( + FileScanTask task, + Schema keySchema, + StructLikeSet keys, + Expression keyFilter, + Map<String, FileMatches> matches) + throws IOException { + DataFile file = task.file(); + + List<DeleteFile> dvs = Lists.newArrayList(); + for (DeleteFile delete : task.deletes()) { + if (ContentFileUtil.isDV(delete)) { + dvs.add(delete); + } else if (delete.content() == FileContent.POSITION_DELETES) { + throw new IllegalStateException( Review Comment: Changed in d90ba035d, a bit differently from the suggestion: nothing is rejected now. For a matched file, the resolver returns all of its existing position deletes, a DV or position delete files, and the new DV merges them, since a DV has to carry every position delete of its data file. `BaseDVFileWriter` reports only file-scoped files as rewritten, so a partition-scoped file stays in the table for its other data files, while reads of this file use the new DV. Existing deletes are read only for files with a matching key, so a legacy delete file no longer blocks unrelated commits. `testMergesPositionDeletesFromFormatVersion2` covers a file-scoped and a partition-scoped file. ########## kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/EqualityDeleteConverter.java: ########## @@ -0,0 +1,336 @@ +/* + * 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.connect.data; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.deletes.BaseDVFileWriter; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.DeleteWriteResult; +import org.apache.iceberg.io.OutputFileFactory; +import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; +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.TypeUtil; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.iceberg.util.StructLikeWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves equality delete files against the rows of a table and writes the matching row positions + * as deletion vectors, so the equality delete files never have to be committed. + * + * <p>An equality delete written by the sink deletes every row of an earlier commit whose identifier + * fields equal one of the deleted keys. The converter reads the keys, groups them by equality + * fields and partition, and asks a {@link KeyPositionResolver} for the rows that hold them. The + * default resolver, {@link ScanKeyPositionResolver}, scans the data files that the column + * statistics cannot rule out; an index-backed resolver can be plugged in instead. A data file that + * already has a deletion vector gets a merged one; the replaced vector is reported in {@link + * Result#rewrittenDvFiles()} and must be removed by the commit. + * + * <p>The positions are only valid for the snapshot they were resolved against, see {@link + * Result#baseSnapshotId()}. The commit that adds the deletion vectors has to validate that no + * conflicting data or delete files were added since then and re-run the conversion otherwise. + * {@link Result#conflictFilter()} narrows that validation to the deleted keys. + * + * <p>Requires format version 3. + */ +public class EqualityDeleteConverter { + + private static final Logger LOG = LoggerFactory.getLogger(EqualityDeleteConverter.class); + + private final Table table; + private final String branch; + private final KeyPositionResolver resolver; + private final DeleteLoader deleteLoader; + private final AtomicInteger scannedDataFiles = new AtomicInteger(); + + /** + * Creates a converter that finds the rows by scanning data files. + * + * @param table the table to resolve the deletes against + * @param branch the branch that is written, or null for the main branch + */ + public EqualityDeleteConverter(Table table, String branch) { + this(table, branch, new ScanKeyPositionResolver(table)); + } + + EqualityDeleteConverter(Table table, String branch, KeyPositionResolver resolver) { + this.table = table; + this.branch = branch; + this.resolver = resolver; + this.deleteLoader = new BaseDeleteLoader(deleteFile -> table.io().newInputFile(deleteFile)); + } + + /** + * Converts the given equality delete files into deletion vectors on the current snapshot of the + * branch. The table is expected to be refreshed by the caller. + */ + public Result convert(List<DeleteFile> eqDeleteFiles) { + Snapshot base = branch == null ? table.currentSnapshot() : table.snapshot(branch); + if (base == null) { + // there are no rows an equality delete could remove + LOG.info( + "Table {} has no snapshot on branch {}, {} equality delete file(s) match no rows", + table.name(), + branch == null ? "main" : branch, + eqDeleteFiles.size()); + return Result.empty(); + } + + Map<DeleteGroup, List<DeleteFile>> groups = groupDeleteFiles(eqDeleteFiles); + + OutputFileFactory fileFactory = + OutputFileFactory.builderFor(table, 1, System.currentTimeMillis()) + .defaultSpec(table.spec()) + .operationId(UUID.randomUUID().toString()) + .format(FileFormat.PUFFIN) + .build(); + // deletion vectors already attached to a matched data file, folded into the new vector + Map<String, PositionDeleteIndex> previousDeletes = Maps.newHashMap(); + BaseDVFileWriter dvWriter = new BaseDVFileWriter(fileFactory, previousDeletes::get); + + Expression conflictFilter = Expressions.alwaysFalse(); + int comparisonsPerFile = KeyFilters.comparisonsPerFile(base); + long matchedRows = 0L; + long deletedKeys = 0L; + scannedDataFiles.set(0); + + long start = System.currentTimeMillis(); + try { + for (Map.Entry<DeleteGroup, List<DeleteFile>> entry : groups.entrySet()) { + DeleteGroup group = entry.getKey(); + Schema keySchema = keySchema(group.equalityFieldIds()); + StructLikeSet keys = deleteLoader.loadEqualityDeletes(entry.getValue(), keySchema); + if (keys.isEmpty()) { + continue; + } + + deletedKeys += keys.size(); + conflictFilter = + Expressions.or( + conflictFilter, + Expressions.and( + KeyFilters.partitionFilter(table.schema(), group.spec(), group.partition()), + KeyFilters.keyFilter(keySchema, keys, comparisonsPerFile))); + + KeyPositionResolver.Resolution resolution = + resolver.resolve(base, keySchema, keys, group.spec(), group.partition()); + scannedDataFiles.addAndGet(resolution.scannedDataFiles()); + + for (KeyPositionResolver.FileMatches matches : resolution.matches()) { + if (matches.existingDeletes() != null) { Review Comment: I went with the first option in 6b1d4c870: `FileMatches.existingDeletes()` now says it returns null only when the file has no position deletes, and what breaks otherwise. Since d90ba035d this includes position delete files left from v2. Returning null for those wouldn't leave two DVs, but readers would ignore the old files once the new DV exists, so the rows they deleted would come back. The scan resolver takes these deletes from the scan tasks it already plans, so I kept the lookup there instead of adding a second one to the converter. ########## kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/EqualityDeleteConverter.java: ########## @@ -0,0 +1,336 @@ +/* + * 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.connect.data; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.deletes.BaseDVFileWriter; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.DeleteWriteResult; +import org.apache.iceberg.io.OutputFileFactory; +import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; +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.TypeUtil; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.iceberg.util.StructLikeWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves equality delete files against the rows of a table and writes the matching row positions + * as deletion vectors, so the equality delete files never have to be committed. + * + * <p>An equality delete written by the sink deletes every row of an earlier commit whose identifier + * fields equal one of the deleted keys. The converter reads the keys, groups them by equality + * fields and partition, and asks a {@link KeyPositionResolver} for the rows that hold them. The + * default resolver, {@link ScanKeyPositionResolver}, scans the data files that the column + * statistics cannot rule out; an index-backed resolver can be plugged in instead. A data file that + * already has a deletion vector gets a merged one; the replaced vector is reported in {@link + * Result#rewrittenDvFiles()} and must be removed by the commit. + * + * <p>The positions are only valid for the snapshot they were resolved against, see {@link + * Result#baseSnapshotId()}. The commit that adds the deletion vectors has to validate that no + * conflicting data or delete files were added since then and re-run the conversion otherwise. + * {@link Result#conflictFilter()} narrows that validation to the deleted keys. + * + * <p>Requires format version 3. + */ +public class EqualityDeleteConverter { + + private static final Logger LOG = LoggerFactory.getLogger(EqualityDeleteConverter.class); + + private final Table table; + private final String branch; + private final KeyPositionResolver resolver; + private final DeleteLoader deleteLoader; + private final AtomicInteger scannedDataFiles = new AtomicInteger(); Review Comment: Done in e29acfc9a: the count is returned on `Result`, and the field and the test-only accessor are gone. 3cecc5f9c later renames it to `plannedDataFiles` (see the thread on the partition tests). -- 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] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
