Uh oh!
There was an error while loading. Please reload this page.
Core: Add V4 scan task planner - #17541
Conversation
anoopj
commented
Aug 6, 2026
cc @danielcweeks @@amogh-jahagirdar@stevenzwu for review. |
| CloseableIterable<FileScanTask> planFiles() { | ||
| List<CloseableIterable<FileScanTask>> taskGroups = Lists.newArrayList(); | ||
| List<TrackedFile> dataFiles = Lists.newArrayList(); |
There was a problem hiding this comment.
Note to reviewers: we are buffering the data files in the root in memory (leaves are lazily expanded). I think it should be okay and not worth making it lazy, but open to revisiting this.
| * must be applied before this planner is wired into a scan or the delete-manifest matching path, | ||
| * since delete scoping compares a data file's sequence number against the delete's. | ||
| */ | ||
| class ManifestExpander { |
There was a problem hiding this comment.
Naming feels wrong here. Maybe ManifestScanner? Expand doesn't really make sense in terms of processing metadata and producing tasks.
There was a problem hiding this comment.
Yeah agree with the naming concern, just tossing another one TaskPlanner?
There was a problem hiding this comment.
Why wouldn't this be implemented within the DataTableScan -> BaseTableScan -> SnapshotScan hierarchy?
There was a problem hiding this comment.
Actually, it's hard to see if this abstraction is right without seeing how it's consumed, do we have a PR that shows how this is used in the scan.planFiles() flow?
Edit: Sorry I was reviewing around the same time @danielcweeks was apparently, but effectively same question as he had ^^.
There was a problem hiding this comment.
This is a great question. The intent here is that this is the V4 analog of the existing package-private ManifestGroup helper, and not a new node in the scan hierarchy.
Today DataTableScan.doPlanFiles() builds a ManifestGroup and calls .planFiles() to turn manifests into FileScanTasks. ManifestExpander plays the same role for the V4 metadata tree: doPlanFiles() (or a doPlanFilesV4() branch, dispatched on format version) will build one and call planFiles(), exactly mirroring the current flow.
So it lives below the scan hierarchy and is consumed by rather than being implemented within DataTableScan/BaseTableScan/SnapshotScan themselves.
You can see how this could be used in the prototype PR here.
There was a problem hiding this comment.
Naming feels wrong here.
Expansion is a terminology we used in a past system that I worked on. I realize it's not standard database terminology, so we can change it. Looking for more suggestions from others.
There was a problem hiding this comment.
I would be good with some name along the line of planner, like ScanTaskPlanner.
There was a problem hiding this comment.
I liked ScanTaskPlanner the best. Renamed.
| * must be applied before this planner is wired into a scan or the delete-manifest matching path, | ||
| * since delete scoping compares a data file's sequence number against the delete's. | ||
| */ | ||
| class ManifestExpander { |
There was a problem hiding this comment.
Yeah agree with the naming concern, just tossing another one TaskPlanner?
| private final FileIO io; | ||
| private final InputFile rootManifest; | ||
| private final Map<Integer, PartitionSpec> specsById; | ||
| private final String tableLocation; |
There was a problem hiding this comment.
Is this needed for handling relative paths?
There was a problem hiding this comment.
Yes, we need pipe it to the reader so that the path resolution happens.
| // delete content is only produced by upgraded trees; the 2-phase path is not built | ||
| // yet. |
There was a problem hiding this comment.
I'm not sure I follow this comment? This abstraction is for V4 entries only I thought, but for those we wouldn't need to two-phase plan right?
There was a problem hiding this comment.
Ok but a V4 entry could be V3 leaf data manifests or V3 delete manifests , so I think this comment is just saying we're not quite handling it here in this code yet.
There was a problem hiding this comment.
Correct. Improved the comment a bit.
| * must be applied before this planner is wired into a scan or the delete-manifest matching path, | ||
| * since delete scoping compares a data file's sequence number against the delete's. | ||
| */ | ||
| class ManifestExpander { |
There was a problem hiding this comment.
Actually, it's hard to see if this abstraction is right without seeing how it's consumed, do we have a PR that shows how this is used in the scan.planFiles() flow?
Edit: Sorry I was reviewing around the same time @danielcweeks was apparently, but effectively same question as he had ^^.
| boolean caseSensitive, | ||
| ScanMetrics scanMetrics, | ||
| ExecutorService executorService) { | ||
| this.io = io; |
There was a problem hiding this comment.
I think we also want to include a reference to the Table being scanned (that's consistent with other existing implementations). Keeping the FileIO separate is necessary for scan/planning which doesn't use the table's FileIO/credentials. I believe the table reference is also used extensively for things like converting to distributed plan and metadata tables. (This would be required with the existing scan heirerarchy).
There was a problem hiding this comment.
I kept the surface minimal here to mirror ManifestGroup since this PR is just the standalone planner. You're right that the Table reference becomes necessary for the pieces that consume it e.g. distributed plan conversion, but those live in the scan-wiring layer that isn't in this PR yet.
I'd prefer to add the Table reference when that wiring lands and actually uses it.
There was a problem hiding this comment.
I think this is good for now. I generally try to avoid passing Table around if we don't need it.
| if (!dataFiles.isEmpty()) { | ||
| taskGroups.add( | ||
| CloseableIterable.transform( | ||
| CloseableIterable.withNoopClose(dataFiles), this::createTask)); | ||
| } | ||
| for (TrackedFile leaf : leafManifests) { | ||
| taskGroups.add(createLeafTasks(leaf)); | ||
| } | ||
| if (executorService != null) { | ||
| return new ParallelIterable<>(taskGroups, executorService); | ||
| } |
There was a problem hiding this comment.
This stacks the immediately available root tasks with the lazy leaf processing in the same parallel backend. I would think you would compose this with the root tasks not going to the parallel backend followed by the parallel iterable.
| this.caseSensitive = caseSensitive; | ||
| this.scanMetrics = scanMetrics; | ||
| this.executorService = executorService; | ||
| this.taskContextCache = Caffeine.newBuilder().build(this::newTaskContext); |
There was a problem hiding this comment.
Why would we use caffine for this? There's no eviction/expiry/size and a tables really don't have that many specs. Just a map would be better here.
There was a problem hiding this comment.
You are correct: Caffeine is an overkill. Changed to a map. I modeled this after ManifestGroup, which does the exact same thing. But probably not worth fixing ManifestGroup to avoid potential regressions.
| + leaf.location()); | ||
| } | ||
| // applying manifest DV during expansion is not supported yet, so reject leaf with manifest DVs. |
There was a problem hiding this comment.
These comments aren't really helpful and just restate code. I'd drop the obvious ones like this.
| // the reader is opened and the leaf counted as scanned lazily, only when the result is | ||
| // iterated: a plan that is built and closed without iterating reads nothing, and each leaf | ||
| // increments scannedDataManifests exactly once. |
There was a problem hiding this comment.
This is unnecessary commentary.
Uh oh!
There was an error while loading. Please reload this page.
| * assigned the data/file sequence numbers, snapshot id, and first-row-id they inherit from their | ||
| * parent, so {@code data_sequence_number}, {@code file_sequence_number}, and row-lineage columns | ||
| * ({@code _row_id}, {@code _last_updated_sequence_number}) read null for added files. Inheritance | ||
| * must be applied before this planner is wired into a scan or the delete-manifest matching path, |
There was a problem hiding this comment.
I assume distributed scan planning will be implemented in another class for v4.
There was a problem hiding this comment.
I think yes, it will be in another class. similar to what we do currently.
| } | ||
| } | ||
| private FileScanTask createTaskFromLeafEntry(TrackedFile entry) { |
There was a problem hiding this comment.
nit: we often use the term "leaf manifest". should we call this createTaskFromDataFileEntry?
| * must be applied before this planner is wired into a scan or the delete-manifest matching path, | ||
| * since delete scoping compares a data file's sequence number against the delete's. | ||
| */ | ||
| class ManifestExpander { |
There was a problem hiding this comment.
I would be good with some name along the line of planner, like ScanTaskPlanner.
| * since delete scoping compares a data file's sequence number against the delete's. | ||
| */ | ||
| class ScanTaskPlanner { | ||
| private static final int FORMAT_VERSION = 4; |
There was a problem hiding this comment.
I would move this constant to TableMetadata and rename it to sth like in my PR.
| CloseableIterable<FileScanTask> rootTasks = | ||
| CloseableIterable.transform(CloseableIterable.withNoopClose(dataFiles), this::createTask); | ||
| List<CloseableIterable<FileScanTask>> leafTasks = Lists.newArrayList(); |
There was a problem hiding this comment.
nit: I would rename leafTasks to leafManifestTasks to make it more clear.
| private CloseableIterable<FileScanTask> createLeafTasks(TrackedFile leaf) { | ||
| // an upgraded tree can reference a legacy-format leaf | ||
| if (leaf.formatVersion() != FORMAT_VERSION) { |
There was a problem hiding this comment.
nit: switch to Preconditions.checkArguement. same for other validations below.
There was a problem hiding this comment.
I switched the nested manifest check to Preconditions.checkArgument because it's a real validation . I left the others as UnsupportedOperationException on purpose: format-version, manifest-DV, because checkArgument would throw IllegalArgumentException and change that meaning.
Happy to reconsider if you'd prefer them as argument checks, but I think the UOE better signals that the input is valid, but the implementation can't support it yet.
| private FileScanTask createTaskFromDataFileEntry(TrackedFile entry) { | ||
| // the tree is at most two levels, so a leaf holds only DATA entries | ||
| if (entry.contentType() == FileContent.DATA_MANIFEST) { |
There was a problem hiding this comment.
nit: similar switch to Preconditions.checkArgument
| @ParameterizedTest | ||
| @FieldSource("MANIFEST_FORMATS") | ||
| void rootWithDataManifestExpandsLeaf(FileFormat format) throws IOException { |
There was a problem hiding this comment.
nit: rootWithLeafManifestEntriesOnly
| // without iterating scans only the root, not the leaf | ||
| planner.planFiles().close(); | ||
| assertThat(metrics.scannedDataManifests().value()).isEqualTo(1L); |
There was a problem hiding this comment.
this remind me that we should probably add scannedDataManifests check for other normal read tests. e.g. leaf manifest read should have value 2.
There was a problem hiding this comment.
Added a scannedDataManifests assertion to rootWithLeafManifestEntriesOnly
| writeManifest( | ||
| FileFormat.AVRO, | ||
| EMPTY_PARTITION, | ||
| ImmutableList.of(dataFile("data-from-avro-leaf.parquet", EMPTY_PARTITION_DATA))); |
There was a problem hiding this comment.
parquet is actually correct since this is the file name of the data file (which happens to come from an Avro manifest
| /* contentType= */ FileContent.DATA_MANIFEST, | ||
| /* formatVersion= */ formatVersion, | ||
| /* location= */ location, | ||
| /* fileFormat= */ FileFormat.PARQUET, |
There was a problem hiding this comment.
leaf manifest file is hardcoded to Parquet here. The Avro leaf test is not actually applied properly.
There was a problem hiding this comment.
Done. They derive file format from location now.
Uh oh!
There was an error while loading. Please reload this page.
| @ParameterizedTest | ||
| @FieldSource("MANIFEST_FORMATS") | ||
| void scannedManifestsCountsRootAndLeaves(FileFormat format) throws IOException { |
There was a problem hiding this comment.
if other tests check scanned metrics, we don't need to repeat this scenario here.
There was a problem hiding this comment.
Yes, it is redundant. Removed scannedManifestsCountsRootAndLeaves
| * their leaf manifests. A data entry's colocated deletion vector is attached to its task as a | ||
| * {@link DeleteFile}. | ||
| * | ||
| * <p>Emitted tasks do not yet carry inherited tracking: leaf and root {@code DATA} entries are not |
There was a problem hiding this comment.
I don't think that this comment is helpful Javadoc. It is not this class's responsibility to handle inheritance and filling in values like first-row-id, that's the responsibility of readers. We don't need documentation that states what a class isn't responsible for.
If the purpose of this is to note that this isn't currently done and it is relevant that this is missing, then it should be in the PR description, or could be noted in non-Javadoc comments where it is relevant.
There was a problem hiding this comment.
Removed . The gap is already in the PR description.
| static Builder builder( | ||
| FileIO io, | ||
| InputFile rootManifest, |
There was a problem hiding this comment.
It's strange to pass both FileIO and an InputFile instance. I would expect a ManifestFile reference instead for this. The io can be used to create an input file.
There was a problem hiding this comment.
Changed it such that he planner takes the root manifest location now and opens it with the provided FileIO. I'll switch it to ManifestFile in a follow-up once the root encryption/keyMetadata path is sorted.
| this.executorService = executorService; | ||
| } | ||
| static Builder builder( |
There was a problem hiding this comment.
What criteria did you use to decide what is passed to create the builder vs passed to configure the builder?
There was a problem hiding this comment.
I added factor args for the ones without any sensible default. This is also aligned to the builder in V4ManifestReader
There was a problem hiding this comment.
I'm removing tableLocation from the manifest reader builder, so I think we should remove it here as well. It seems okay to pass specs, but I think we could default that to null. We can remove it later if we want to.
| leafManifests.add(entry); | ||
| break; | ||
| default: | ||
| // delete content appears only on upgraded trees |
There was a problem hiding this comment.
What does this mean? Is this to simplify and not support upgraded tables until later?
There was a problem hiding this comment.
Yes, this only supports fully native v4 trees. Clarified the comment
| } | ||
| V4ManifestReader build() { | ||
| if (manifestDv != null) { |
There was a problem hiding this comment.
What's the point of the changes in this file? I guess it's setting the stage for some follow-up change, but at this point I don't think it adds any value.
There was a problem hiding this comment.
Until we implement MDV, the rejection has to be done somewhere. The initial version of the PR had it done in the scan task planner: this is moving the rejection to the correct place (reader)
| * their leaf manifests. A data entry's colocated deletion vector is attached to its task as a | ||
| * {@link DeleteFile}. | ||
| */ | ||
| class ScanTaskPlanner { |
There was a problem hiding this comment.
Just for my benefit to see the bigger picture: Is this meant to replace the functionality in SnapshotScan.doPlanFiles() and in ManifestGroup.planFiles() and such? I'd like to understand how this fits into the flow that is executed when a user runs table.newScan().planFiles(). Is there going to be an if on the table version to branch which code path is run?
There was a problem hiding this comment.
That is correct. We will probably have a version specific branch in table scan layer. e.g. DataTableScan
There was a problem hiding this comment.
I general comment about this class after taking a second look: I think this serves 2 different purposes merged into one.
- Provide a "root manifest reader" that takes the location of the root and reads all the
TrackedFiles including the ones in the leaves. - Do scan planning on top of the
TrackedFileiterable we receive from the "root manifest reader"
I'd bet that we want to read the manifest tree for multiple purposes not just to perform "scan task planning" so I'm wondering if we can split the root reader part into some other class that can be reused ?
Another general, maybe unrelated question is if want to still support Snapshot's allManifest(), dataManifests(), deleteManifests() functionality for V4.
| // out; direct DATA entries are buffered too, bounded by how many data files a tree keeps | ||
| // directly in the root. Leaf tasks stay lazy: planLeaf opens no reader until iterated. | ||
| scanMetrics.scannedDataManifests().increment(); | ||
| try (CloseableIterable<TrackedFile> rootEntries = |
There was a problem hiding this comment.
Root manifest file can be encrypted. I see an exception throws for encrypted leaf manifests, for consistency shouldn't we Thor one here, or let the reader fail anyway because it's unable to parse the file?
There was a problem hiding this comment.
We will add encryption support soon as a followup. Until then would it be reasonable to just let the reader fail?
There was a problem hiding this comment.
I don't think we handle the lack of encryption support consistently. For root manifest we leave the reader/parser to fail, while for leaf manifests we have a check and throw an exception ourselves.
| private final ExecutorService executorService; | ||
| private final Map<Integer, TaskContext> taskContextsBySpec = Maps.newConcurrentMap(); | ||
| private ScanTaskPlanner( |
There was a problem hiding this comment.
Trying to wrap my head around encrypted root manifests. For that we'd need a couple of more inputs here. If I'm not mistaken, it would go through a similar mechanism as the pre-V4 manifest list location: it'd require a key-id from the snapshot, the encryption-keys from TableMetadata and maybe other stuff.
I'm wondering if we want to pass the proposed new interface for "files encrypted with key-id" (PR here) instead of rootManifestLocation
There was a problem hiding this comment.
The PR doesn't handle encryption yet. In a followup, we will be converting this into a new ManifestFIle-like class that supports encryption. I think the abstraction you are adding in #17545 will be used here.
There was a problem hiding this comment.
@gaborkaszab, we have been moving toward passing files to methods in EncryptingFileIO and I think that the root metadata would be handled similarly. EncryptingFileIO sets up the correct InputFile that carries the key metadata. We should continue to use this strategy to avoid needing to handle EncryptionManager in lots of different places.
For the root metadata file specifically, we will need to create a ManifestFile that represents it. I think that when we create that ManifestFile is when we will unwrap and resolve the key-id to key metadata.
Your PR is also on my TODO list for reviews, so I apologize if you've already thought through this and I'm repeating what you already know.
There was a problem hiding this comment.
You're right, passing the right EncryptingFileIO to this class will take care of decrypting the root manifest file.
| leafDataEntries.add(planLeaf(leaf)); | ||
| } | ||
| CloseableIterable<TrackedFile> expandedLeafEntries = |
There was a problem hiding this comment.
If I'm not mistaken, this is able to read a 2-level metadata tree: root and leaves. I recall there was a discussion that theoretically we can have more levels in the tree. I'm not sure if there is anything in the proposed spec or anywhere that prevents writers doing that, but if not, I'm wondering we should be flexible enough on the read path to support such a layout.
There was a problem hiding this comment.
V4 we intentionally are keeping it at two levels. For tables with multi-billion files, we will need to get to three levels or more, but that is outside the scope of v4.
There was a problem hiding this comment.
I've just checked the spec proposal, and while we don't explicitly sat the tree was to have 2 levels, the way how we describe root manifest and data/delete manifests implies that there can't be more level ATM.
| * their leaf manifests. A data entry's colocated deletion vector is attached to its task as a | ||
| * {@link DeleteFile}. | ||
| */ | ||
| class ScanTaskPlanner { |
There was a problem hiding this comment.
Naming: I don't think this should identify ScanTask specifically. That's the root interface of the task hierarchy, which includes changelog task types, combined task types, and the simplest FileScanTask.
The purpose of this class is to produce matching FileScanTask instances for a specific metadata tree root. Maybe a better name for this would be FilePlanner since this implements planFiles.
There was a problem hiding this comment.
Makes sense. An alternative isFileScanTaskPlanner that implements planFileScanTasks()? I have a slight preference for this name.
| // root is drained into these lists. Leaf references must be buffered so they can be fanned | ||
| // out; direct DATA entries are buffered too, bounded by how many data files a tree keeps | ||
| // directly in the root. Leaf tasks stay lazy: planLeaf opens no reader until iterated. |
There was a problem hiding this comment.
This comment applies to the lists above and the try block, but not the statement that it precedes.
It is also too long. It's obvious from the code that the data files and leaf manifests are kept in a list. It's also pretty obvious that the leaf manifests are going to be read as tasks and that we aren't going to emit the data files immediately. This comment doesn't seem very insightful or helpful to me.
| leafManifests.add(entry); | ||
| break; | ||
| default: | ||
| // delete manifests appear only on upgraded v2/v3 tables; that 2-phase path is not | ||
| // supported yet, so a natively-written v4 tree is the only supported input for now | ||
| throw new UnsupportedOperationException( | ||
| "Cannot plan content type in root manifest: " + entry.contentType()); |
There was a problem hiding this comment.
"Unsupported file type in root manifest: %s"
| break; | ||
| default: | ||
| // delete manifests appear only on upgraded v2/v3 tables; that 2-phase path is not | ||
| // supported yet, so a natively-written v4 tree is the only supported input for now |
There was a problem hiding this comment.
Avoid making statements about this point in time because they tend to become stale. It would be easy for someone to add case DELETE_MANIFEST: above and not edit this comment.
I think a better approach is to add a case that specifically calls out what you're only stating in a comment here:
caseDELETE_MANIFEST:
thrownewUnsupportedOperationException("v3 and earlier deletes are not yet supported");
default:
thrownewUnsupportedOperationException("Unsupported file type in root manifest: " + ...);| } | ||
| // read each leaf's data entries lazily; only leaf reads are worth the parallel backend, so the | ||
| // already-in-hand root data entries are concatenated in directly |
There was a problem hiding this comment.
worth the parallel backend
I don't think this comment makes sense. Only leaf manifests need to be read. Someone reading this code doesn't need to think about the data files that were stored in the root, only that this step is producing ClosableIterable instances that will read a leaf manifest file when they are iterated through.
| } | ||
| CloseableIterable<TrackedFile> expandedLeafEntries = | ||
| executorService != null |
There was a problem hiding this comment.
Did this drop the dataManifests.size() > 1 check that is happening in DataTableScan?
| // read each leaf's data entries lazily; only leaf reads are worth the parallel backend, so the | ||
| // already-in-hand root data entries are concatenated in directly | ||
| List<CloseableIterable<TrackedFile>> leafDataEntries = Lists.newArrayList(); |
There was a problem hiding this comment.
Aren't all of these known to be files, and specifically data files? Why call them "entries"?
There was a problem hiding this comment.
Since these are lazy, I would call this leafPlanTask. That mirrors the terminology we use in the server-side planning API.
| CloseableIterable<FileScanTask> planFiles() { | ||
| List<TrackedFile> rootDataFiles = Lists.newArrayList(); | ||
| List<TrackedFile> leafManifests = Lists.newArrayList(); |
There was a problem hiding this comment.
This should be List<ManifestFile>.
| private CloseableIterable<TrackedFile> planLeaf(TrackedFile leaf) { | ||
| // an upgraded tree can reference a legacy-format leaf | ||
| if (leaf.formatVersion() != TableMetadata.MIN_FORMAT_VERSION_ADAPTIVE_MANIFEST_TREE) { |
There was a problem hiding this comment.
It's odd to use != with MIN.
Where should this be enforced? I'd prefer it in the V4ManifestReader, which should be responsible for identifying the versions that it accepts. It could accept 4 and 5 after we release 5 and reject 6 just like TableMetadata would reject 6. I think this should be exposed through ManifestFile and checked there.
| @Override | ||
| public CloseableIterator<TrackedFile> iterator() { | ||
| // pass the known leaf size so the reader sizes the read instead of stat-ing the file |
There was a problem hiding this comment.
This is the responsibility of FileIO: https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/encryption/EncryptingFileIO.java#L127
| public CloseableIterator<TrackedFile> iterator() { | ||
| // pass the known leaf size so the reader sizes the read instead of stat-ing the file | ||
| ByteBuffer manifestDv = leaf.manifestInfo() != null ? leaf.manifestInfo().dv() : null; | ||
| CloseableIterable<TrackedFile> entries = |
| } | ||
| /** Lazily reads one leaf manifest's data entries; each iteration owns and counts its reader. */ | ||
| private class LeafDataEntries extends CloseableGroup implements CloseableIterable<TrackedFile> { |
There was a problem hiding this comment.
I don't think that this class is needed. open produces a V4ManifestReader which is already a CloseableIterable<TrackedFile>. The only thing that this is doing differently is incrementing scannedDataManifests() when iterator is called. But we're already passing the scan metrics to the reader, so it can do that itself if metrics are present.
This is also transforming the iterator, but that doesn't need to be done in this iterator and in fact should not be. This is creating an Iterable and then immediately calling iterator() on it. We should compose iterables, rather than embed them like this to keep these steps independent.
| } | ||
| } | ||
| private CloseableIterable<TrackedFile> open(InputFile manifest, ByteBuffer manifestDv) { |
There was a problem hiding this comment.
This doesn't open the file, it creates a reader. This should probably be called reader.
| .filter(dataFilter) | ||
| .caseSensitive(caseSensitive) | ||
| .scanMetrics(scanMetrics) | ||
| .manifestDv(manifestDv) |
There was a problem hiding this comment.
I think the manifest DV should be passed into the reader via ManifestFile, which is where it will live. I'm also skeptical that it will be passed as ByteBuffer, so we should work on getting the mumbling implementation in. That way we can use a real bitmap.
| .build(); | ||
| } | ||
| private TrackedFile requireDataEntry(TrackedFile entry) { |
There was a problem hiding this comment.
We don't use this pattern of returning a validated object.
| if (entry.contentType() != FileContent.DATA) { | ||
| throw new UnsupportedOperationException( | ||
| "Cannot plan content type in leaf manifest: " + entry.contentType()); | ||
| } |
There was a problem hiding this comment.
Are you trying to have a more specific error message for manifests found in leaf files?
It doesn't make much sense to do this check twice. I'm also skeptical that this is the responsibility of the planner. Shouldn't this be done by a leaf manifest reader? Verifying that the files in a manifest are valid should be the reader's job.
| private TrackedFile requireDataEntry(TrackedFile entry) { | ||
| // the tree is at most two levels, so a leaf holds only DATA entries | ||
| Preconditions.checkArgument( |
| } | ||
| private FileScanTask createTask(TrackedFile trackedFile) { | ||
| DataFile dataFile = TrackedFileAdapters.asDataFile(trackedFile, specsById); |
There was a problem hiding this comment.
Just a heads-up from wiring the v4 read path through Spark's DSv2 scan, once a FileScanTask emitted by the v4 planner reaches Spark, its DataFile gets serialized to executors. The tracked-manifest read adapter that presents a v4 row as a DataFile needs to be Serializable for that to work.
Reintroduces the ManifestFile adapter closed in #16867, which stalled because ManifestFile had no way to represent a manifest deletion vector. The V4 scan task planner (#17541) plans over manifests through the ManifestFile API, so this adds ManifestFile.manifestDeletionVector() and has the adapter expose the tracked file's manifest DV.
Adds the planner that walks a v4 adaptive metadata tree and emits
FileScanTasks. It reads the root manifest, emits a task per live DATA entry, and expands DATA_MANIFEST entries into their leaf manifests. Leaves are read lazily and, when a worker pool is supplied, expanded in parallel.Keeping it package-private and not wired into any scan yet.
Support for the following features will be added in followup PRs.