This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 847da32d2bd [fix](iceberg) Make rewrite_manifests rewrite manifests
and count what it did (#69076)
847da32d2bd is described below
commit 847da32d2bde974d5ada37169f2d77d17121d949
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Oct 16 19:14:08 2026 +0800
[fix](iceberg) Make rewrite_manifests rewrite manifests and count what it
did (#69076)
### What problem does this PR solve?
Issue Number: None
Related PR: #59487
Problem Summary:
**In short.** The Iceberg `rewrite_manifests` procedure committed a
snapshot without rewriting any manifest, and still reported every
targeted manifest as rewritten. This PR clusters the data files by runs
of adjacent values of their first partition field, as Trino does, so the
rewrite actually merges manifests with a bounded number of manifest
writers, and returns the numbers of manifests it replaced and wrote. A
unit test written for #68902 turned this up; branch-4.2 has the same
code.
**Background.** `ALTER TABLE ... EXECUTE rewrite_manifests(['spec_id' =
...])` runs `RewriteManifestExecutor` on Iceberg's `RewriteManifests`
API and returns `rewritten_manifests_count` and `added_manifests_count`.
That API merges manifests only when a cluster function is set with
`clusterBy`. Without one, `BaseRewriteManifests.requiresRewrite` returns
false and the commit keeps every current manifest as it is.
**The problem, and what it cost.** The executor never set a cluster
function, so the procedure changed nothing while reporting that it had.
On a local single-BE cluster with a file:// Iceberg hadoop catalog:
| Table | master | with this PR |
|---|---|---|
| Unpartitioned, three INSERTs (3 manifests) | `[3, 0]`, still 3
manifests | `[3, 1]`, 1 manifest |
| Partitioned by `(a, b)`, four INSERTs into `(1, x)`, `(1, y)`, `(2,
x)`, `(2, y)` (4 manifests) | `[4, 0]`, still 4 manifests | `[4, 1]`, 1
manifest: the four fit in one target-size manifest |
| Partitioned by `a`, four INSERTs of `a` = 100, 1, 50, 7,
`commit.manifest.target-size-bytes` set to half the size of the 4
manifests | `[4, 0]`, still 4 manifests | `[4, 2]`, 2 manifests covering
`a` 1 to 7 and 50 to 100 |
| One INSERT (1 small manifest) | `[1, 0]`, plus a new snapshot that
changes nothing | `[0, 0]`, no new snapshot |
Running the procedure to compact metadata never reduced the number of
manifests. Every run on a table with one manifest also added a snapshot.
**How this PR fixes it.**
- **Clustering.** The executor sets `clusterBy` to runs of adjacent
values of the data file's first partition field, as Trino's
`optimize_manifests` does. A partition spec whose targeted manifests are
larger than `commit.manifest.target-size-bytes` (8 MB by default) gets
`min(ceil(their size / target size), 200)` clusters: the executor reads
the distinct values of the spec's first partition field from those
manifests, sorts them with NULL first, and cuts them into that many runs
of about equal length. Adjacent partitions therefore share a manifest,
which keeps each manifest's partition summary narrow, and the rewrite
keeps at most that many manifest writers open for the spec; Iceberg
keeps one open per cluster until the scan ends. A spec whose targeted
manifests fit in one target-size manifest, or an unpartitioned spec, is
one cluster and is not read ahead, so such a table ends up with one
manifest. Iceberg still rolls each cluster over at the target size.
Unlike Trino, which reads every file's partition as the current spec's,
each spec is clustered on its own first field, because `spec_id` can
target an older spec.
- **Single small manifest.** As in Spark's `RewriteManifestsSparkAction`
and in Trino, a single targeted manifest smaller than the target size is
left alone: nothing is committed and the result is `[0, 0]`.
- **Only the targeted manifests.** The rewrite rewrites only the
manifests it targeted when it started, by path, which also applies
`spec_id`. A manifest that another writer commits in between is kept as
it is. Rewriting it failed when it was of a partition spec added after
the table was loaded, because Iceberg's `RewriteManifests` takes the
specs of the table it is created from.
- **Counts.** `rewritten_manifests_count` and `added_manifests_count`
are `manifests-replaced` and `manifests-created` of the snapshot this
rewrite committed, read from the event that Iceberg builds for it after
the commit. The table's current snapshot after the commit can already be
another writer's.
- `spec_id` still limits the rewrite to the manifests of one spec, and
delete manifests are kept as they are, as Iceberg's rewrite does.
Spark's action instead sorts every entry by partition and splits the
entries into `ceil(total size / target size)` manifests. In Spark that
sort is distributed, but here the FE would have to hold every entry in
memory first. This PR keeps Iceberg's streaming cluster function and,
like Trino, holds only the distinct values of the first partition field.
Reading them costs one more pass over the targeted manifests, with only
the partition column projected, and only for a spec whose manifests
exceed the target size.
**Results.**
- **FE UT.** These cases in `IcebergRewriteManifestsActionTest` fail on
master and pass with this PR:
- three appends become one manifest, `[3, 1]`;
- four manifests of a two-field partition that fit in one target-size
manifest become one, `[4, 1]`;
- with the target size set for two clusters, files appended with 100, 1,
50 and 7 become the runs {1, 7} and {50, 100}, `[4, 2]`;
- two partitions that one commit wrote into the same manifest are split
by value, `[2, 2]`;
- a NULL first partition value sorts first and takes the first of two
clusters, `[3, 2]`;
- the manifests of two partition specs, one on an `int` field and one on
a `string` field, are clustered each by its own first field, `[4, 4]`;
- a single small manifest is left alone, `[0, 0]`, with no new snapshot.
On master the clustering cases return `[N, 0]` and the last returns `[1,
0]`. Two cases cover the cluster count (`min(ceil(size / target), 200)`,
without overflow) and the runs (1000 values in 200 clusters, five
adjacent values each, NULL first). Two more cover a writer that commits
after the table was loaded: an appended manifest is kept and the result
is `[2, 1]`, where counting from the table's manifests gave `[2, 2]` and
rewriting every manifest gave `[3, 1]`; and a manifest of a partition
spec added after the load is kept, where the rewrite failed with a
NullPointerException. Another writer promoting the first partition field
from `int` to `long` during the rewrite makes it fail before it commits,
leaving the table as it was. Not sorting, dropping the cap of 200,
rounding up in a way that overflows, clustering every spec on the
current spec's first field, and sorting without NULL first each turn a
case red. All 1540 tests of fe-connector-iceberg pass.
- **End to end.** The table above was produced on a local cluster. After
the rewrite, the four tables still return 3, 4, 1 and 4 rows.
- **Regression.** `test_iceberg_rewrite_manifests` failed on this PR at
its `spec_id` case: the first INSERT after the partition spec changes
merges the three manifests of spec 0 into one, because Iceberg's merge
append compacts the manifests of a spec that the commit does not write
to, and the case rewrote that single small manifest expecting a count of
1. The case now records the manifests per spec in the `.out` (one for
spec 0, two for spec 1), rewrites spec 1 (`[2, 1]`, after which each
spec has one manifest), and checks that rewriting the single small
manifest of spec 0 returns `[0, 0]`. Its other count checks hold with
the new counts. The suite passes on a local cluster against the Iceberg
REST docker environment, where the `.out` was generated.
---
.../iceberg/action/RewriteManifestExecutor.java | 189 +++++++++--
.../connector/iceberg/action/ActionTestTables.java | 4 +
.../action/IcebergRewriteManifestsActionTest.java | 374 ++++++++++++++++++++-
.../action/test_iceberg_rewrite_manifests.out | 14 +
.../action/test_iceberg_rewrite_manifests.groovy | 44 ++-
5 files changed, 559 insertions(+), 66 deletions(-)
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/RewriteManifestExecutor.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/RewriteManifestExecutor.java
index 31f5b450715..692c58ac85b 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/RewriteManifestExecutor.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/RewriteManifestExecutor.java
@@ -19,26 +19,57 @@ package org.apache.doris.connector.iceberg.action;
import org.apache.doris.connector.spi.DorisConnectorException;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import org.apache.iceberg.DataFile;
import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.RewriteManifests;
import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.events.CreateSnapshotEvent;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.util.ParallelIterable;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* Executor for manifest rewrite operations. Connector port of legacy
* {@code datasource/iceberg/rewrite/RewriteManifestExecutor}. The fe-core
couplings are dropped: there is no
* {@code ExternalTable} parameter and no {@code
Env.getExtMetaCacheMgr().invalidateTableCache} call (cache
- * invalidation is performed once at dispatch level by {@code
IcebergProcedureOps}); the SDK call chain and
- * the before/after manifest accounting are otherwise verbatim, with the
failure message kept byte-identical
- * to the legacy {@code UserException}.
+ * invalidation is performed once at dispatch level by {@code
IcebergProcedureOps}); the failure message is kept
+ * byte-identical to the legacy {@code UserException}. Unlike the legacy
executor, which committed a rewrite
+ * without a cluster function (so Iceberg kept every manifest as it was), it
clusters the data files by runs of
+ * adjacent values of their first partition field, skips a single manifest
below the target size, and reports the
+ * manifests the rewrite actually replaced and wrote.
*/
public class RewriteManifestExecutor {
private static final Logger LOG =
LogManager.getLogger(RewriteManifestExecutor.class);
+ /**
+ * The most clusters of one partition spec, and so the most manifest
writers that a rewrite keeps open for it,
+ * as in Trino's optimize_manifests: Iceberg's clusterBy keeps one writer
per key open until the scan ends.
+ */
+ private static final int MAX_CLUSTERS_PER_SPEC = 200;
+
public static class Result {
private final int rewrittenCount;
private final int addedCount;
@@ -66,51 +97,135 @@ public class RewriteManifestExecutor {
}
// Collect manifests before rewrite and filter by specId if
provided
- List<ManifestFile> manifestsBefore =
currentSnapshot.dataManifests(table.io());
- List<ManifestFile> manifestsBeforeTargeted =
filterBySpecId(manifestsBefore, specId);
-
- int rewrittenCount = manifestsBeforeTargeted.size();
-
- if (rewrittenCount == 0) {
+ List<ManifestFile> manifestsBeforeTargeted =
+ filterBySpecId(currentSnapshot.dataManifests(table.io()),
specId);
+ if (manifestsBeforeTargeted.isEmpty()) {
return new Result(0, 0);
}
-
- // Configure rewrite operation, optionally restricting manifests
by specId
- RewriteManifests rm = table.rewriteManifests();
-
- if (specId != null) {
- final int targetSpecId = specId;
- rm.rewriteIf(manifest -> manifest.partitionSpecId() ==
targetSpecId);
+ // One manifest below the target size has nothing to merge with,
as in Spark's and Trino's manifest
+ // rewrites: leave the table alone instead of committing a
snapshot that changes nothing.
+ long targetManifestSizeBytes =
PropertyUtil.propertyAsLong(table.properties(),
+ TableProperties.MANIFEST_TARGET_SIZE_BYTES,
TableProperties.MANIFEST_TARGET_SIZE_BYTES_DEFAULT);
+ if (manifestsBeforeTargeted.size() == 1
+ && manifestsBeforeTargeted.get(0).length() <
targetManifestSizeBytes) {
+ return new Result(0, 0);
}
- // Commit manifest rewrite
+ // Without a cluster function Iceberg rewrites nothing
(BaseRewriteManifests.requiresRewrite) and only
+ // commits a snapshot that keeps every manifest. Only the targeted
manifests are rewritten: a manifest
+ // that another writer commits before the rewrite applies is kept
as it is. Such a manifest can be of a
+ // partition spec added after the table was loaded, which the
rewrite cannot write, because it takes
+ // the specs of the table it is created from.
+ Set<String> targetedPaths = manifestsBeforeTargeted.stream()
+ .map(ManifestFile::path)
+ .collect(Collectors.toSet());
+ Map<Integer, Map<Object, Integer>> clustersBySpec =
+ clustersBySpec(table, manifestsBeforeTargeted,
targetManifestSizeBytes);
+ RewriteManifests rm = table.rewriteManifests()
+ .clusterBy(file -> clusterOf(clustersBySpec, file))
+ .rewriteIf(manifest ->
targetedPaths.contains(manifest.path()));
rm.commit();
- // Refresh snapshot after rewrite
- Snapshot snapshotAfter = table.currentSnapshot();
- if (snapshotAfter == null) {
- return new Result(rewrittenCount, 0);
+ // Count the manifests replaced and written by the snapshot this
rewrite committed. Iceberg builds the
+ // event of that snapshot after the commit
(SnapshotProducer.notifyListeners), while the table's current
+ // snapshot can already be another one.
+ Map<String, String> summary = ((CreateSnapshotEvent)
rm.updateEvent()).summary();
+ return new
Result(Integer.parseInt(summary.get(SnapshotSummary.REPLACED_MANIFESTS_COUNT)),
+
Integer.parseInt(summary.get(SnapshotSummary.CREATED_MANIFESTS_COUNT)));
+ } catch (Exception e) {
+ LOG.warn("Failed to execute manifest rewrite for table: {}",
table.name(), e);
+ throw new DorisConnectorException("Failed to rewrite manifests: "
+ e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Groups the data files of each targeted partition spec into runs of
adjacent values of the spec's first
+ * partition field, as Trino's optimize_manifests does. A spec gets
min(ceil(size of its targeted manifests /
+ * target size), 200) clusters: the distinct values are read from its
targeted manifests, sorted with NULL first,
+ * and the value at rank i goes to cluster i * clusters / n. Adjacent
partitions then share a manifest, which
+ * keeps the manifests' partition summaries narrow for pruning, while the
rewrite keeps at most that many
+ * writers open for the spec; Iceberg still rolls each cluster over at the
target size. An unpartitioned spec, or
+ * one whose targeted manifests fit in one target-size manifest, is one
cluster and is not read ahead. Each spec
+ * is clustered on its own first field, which can be of another type than
another spec's: Iceberg keeps a writer
+ * per cluster and spec, so the specs' cluster numbers do not collide.
+ *
+ * @return the value-to-cluster map of each targeted spec, empty for a
spec that is one cluster
+ */
+ private static Map<Integer, Map<Object, Integer>> clustersBySpec(Table
table, List<ManifestFile> targeted,
+ long targetManifestSizeBytes) throws IOException {
+ Map<Integer, List<ManifestFile>> manifestsBySpec = targeted.stream()
+ .collect(Collectors.groupingBy(ManifestFile::partitionSpecId));
+ Map<Integer, Map<Object, Integer>> clustersBySpec = new HashMap<>();
+ for (Map.Entry<Integer, List<ManifestFile>> entry :
manifestsBySpec.entrySet()) {
+ PartitionSpec spec = table.specs().get(entry.getKey());
+ int clusters =
clusterCount(entry.getValue().stream().mapToLong(ManifestFile::length).sum(),
+ targetManifestSizeBytes);
+ if (spec.isUnpartitioned() || clusters == 1) {
+ clustersBySpec.put(entry.getKey(), Collections.emptyMap());
+ continue;
}
+ Comparator<Object> order =
Comparators.<Object>nullsFirst().thenComparing(
+
Comparators.forType(spec.partitionType().fields().get(0).type().asPrimitiveType()));
+ clustersBySpec.put(entry.getKey(),
+ assignClusters(firstPartitionValues(table,
entry.getValue()), order, clusters));
+ }
+ return clustersBySpec;
+ }
- // Collect manifests after rewrite and filter by specId
- List<ManifestFile> manifestsAfter =
snapshotAfter.dataManifests(table.io());
- List<ManifestFile> manifestsAfterTargeted =
filterBySpecId(manifestsAfter, specId);
+ /** min(ceil(manifestsSizeBytes / targetManifestSizeBytes), 200); a
manifest is never empty, so at least 1. */
+ static int clusterCount(long manifestsSizeBytes, long
targetManifestSizeBytes) {
+ return (int) Math.min((manifestsSizeBytes - 1) /
targetManifestSizeBytes + 1, MAX_CLUSTERS_PER_SPEC);
+ }
- // Compute addedCount as newly produced manifests (path not in
before set)
- java.util.Set<String> beforePaths =
manifestsBeforeTargeted.stream()
- .map(ManifestFile::path)
- .collect(java.util.stream.Collectors.toSet());
+ /**
+ * Sorts the values and gives the one at rank i cluster i * clusters / n,
so each cluster is a run of adjacent
+ * values and the runs differ in length by at most one.
+ */
+ static Map<Object, Integer> assignClusters(Set<Object> values,
Comparator<Object> order, int clusters) {
+ List<Object> sorted = new ArrayList<>(values);
+ sorted.sort(order);
+ Map<Object, Integer> clusterOfValue = new HashMap<>();
+ for (int rank = 0; rank < sorted.size(); rank++) {
+ clusterOfValue.put(sorted.get(rank), (int) ((long) rank * clusters
/ sorted.size()));
+ }
+ return Collections.unmodifiableMap(clusterOfValue);
+ }
- int addedCount = (int) manifestsAfterTargeted.stream()
- .map(ManifestFile::path)
- .filter(path -> !beforePaths.contains(path))
- .count();
+ /**
+ * The distinct values of the first partition field among the live data
files of manifests of one spec, read
+ * with only the partition column projected, in parallel on Iceberg's
worker pool as the rewrite itself reads
+ * them. ManifestReader returns copies of the files, and PartitionData
returns immutable values or copies of
+ * binary ones, so the values can be kept and compared by content; a
HashSet holds a NULL value as well.
+ */
+ private static Set<Object> firstPartitionValues(Table table,
List<ManifestFile> manifests) throws IOException {
+ Iterable<ManifestReader<DataFile>> readers =
Iterables.transform(manifests,
+ manifest -> ManifestFiles.read(manifest, table.io(),
table.specs())
+ .select(Collections.singletonList("partition")));
+ Set<Object> values = new HashSet<>();
+ try (CloseableIterable<DataFile> files = new
ParallelIterable<>(readers, ThreadPools.getWorkerPool())) {
+ for (DataFile file : files) {
+ values.add(file.partition().get(0, Object.class));
+ }
+ }
+ return values;
+ }
- return new Result(rewrittenCount, addedCount);
- } catch (Exception e) {
- LOG.warn("Failed to execute manifest rewrite for table: {}",
table.name(), e);
- throw new DorisConnectorException("Failed to rewrite manifests: "
+ e.getMessage(), e);
+ /**
+ * The cluster of a data file. Iceberg calls this from its worker threads
while it reads the targeted manifests,
+ * so it only reads maps built before the commit. Every file it is given
comes from a targeted manifest, whose
+ * values were read ahead; a value missing from the map means the table's
partitioning changed while the
+ * rewrite ran.
+ */
+ private static Object clusterOf(Map<Integer, Map<Object, Integer>>
clustersBySpec, DataFile file) {
+ Map<Object, Integer> clusters = clustersBySpec.get(file.specId());
+ if (clusters.isEmpty()) {
+ return 0;
}
+ Object value = file.partition().get(0, Object.class);
+ Integer cluster = clusters.get(value);
+ Preconditions.checkState(cluster != null, "Partition value %s of %s
was not in the targeted manifests when"
+ + " the rewrite started; the table's partitioning changed
during the rewrite", value, file.location());
+ return cluster;
}
private List<ManifestFile> filterBySpecId(List<ManifestFile> manifests,
Integer specId) {
@@ -120,6 +235,6 @@ public class RewriteManifestExecutor {
final int targetSpecId = specId;
return manifests.stream()
.filter(manifest -> manifest.partitionSpecId() == targetSpecId)
- .collect(java.util.stream.Collectors.toList());
+ .collect(Collectors.toList());
}
}
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/ActionTestTables.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/ActionTestTables.java
index 49ffde1fb47..f0f739b62e2 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/ActionTestTables.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/ActionTestTables.java
@@ -69,6 +69,10 @@ final class ActionTestTables {
return createTable(catalog, table, Collections.emptyMap());
}
+ static Table createTable(InMemoryCatalog catalog, String table,
PartitionSpec spec) {
+ return catalog.createTable(id(table), SCHEMA, spec,
Collections.emptyMap());
+ }
+
/** Appends one data file as a new snapshot and returns the new current
snapshot id. */
static long appendSnapshot(InMemoryCatalog catalog, String table, String
fileName, long records) {
Table t = catalog.loadTable(id(table));
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRewriteManifestsActionTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRewriteManifestsActionTest.java
index 4f538c4ae1e..7e6b4fe0f9e 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRewriteManifestsActionTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRewriteManifestsActionTest.java
@@ -22,12 +22,19 @@ import
org.apache.doris.connector.spi.procedure.ConnectorProcedureResult;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
import org.apache.iceberg.DeleteFiles;
import org.apache.iceberg.ExpireSnapshots;
+import org.apache.iceberg.FileFormat;
import org.apache.iceberg.HistoryEntry;
import org.apache.iceberg.ManageSnapshots;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
import org.apache.iceberg.OverwriteFiles;
+import org.apache.iceberg.PartitionData;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.ReplacePartitions;
import org.apache.iceberg.ReplaceSortOrder;
@@ -40,6 +47,7 @@ import org.apache.iceberg.SnapshotRef;
import org.apache.iceberg.SortOrder;
import org.apache.iceberg.StatisticsFile;
import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
import org.apache.iceberg.TableScan;
import org.apache.iceberg.Transaction;
import org.apache.iceberg.UpdateLocation;
@@ -49,14 +57,23 @@ import org.apache.iceberg.UpdateSchema;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.encryption.EncryptionManager;
import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.io.CloseableIterable;
import org.apache.iceberg.io.FileIO;
import org.apache.iceberg.io.LocationProvider;
+import org.apache.iceberg.types.Comparators;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Pins {@code rewrite_manifests}, including the delegated connector {@link
RewriteManifestExecutor}.
@@ -103,12 +120,357 @@ public class IcebergRewriteManifestsActionTest {
action.validate();
ConnectorProcedureResult result =
action.execute(catalog.loadTable(id), ActionTestTables.session("UTC"));
- // The executor reports manifestsBefore.size() as the rewritten count
(the faithful port behaviour);
- // the added count is iceberg's internal combine outcome, asserted
only as a valid non-negative int.
- Assertions.assertEquals(String.valueOf(manifestsBefore),
result.getRows().get(0).get(0),
- "rewritten_manifests_count is the number of manifests targeted
for rewrite");
- Assertions.assertTrue(Integer.parseInt(result.getRows().get(0).get(1))
>= 0,
- "added_manifests_count is a valid non-negative count");
+ // Iceberg rewrites nothing without a cluster function
(BaseRewriteManifests.requiresRewrite) and only
+ // commits a snapshot that keeps every manifest. With one, the three
manifests of an unpartitioned table
+ // become one, and the counts are the manifests actually replaced and
written.
+ Assertions.assertEquals(ImmutableList.of("3", "1"),
result.getRows().get(0));
+ Table after = catalog.loadTable(id);
+ Assertions.assertEquals(1,
after.currentSnapshot().dataManifests(after.io()).size());
+ Assertions.assertEquals(3,
Iterables.size(after.newScan().planFiles()), "every data file is kept");
+ }
+
+ @Test
+ public void mergesPartitionedManifestsThatFitTheTargetSizeIntoOne() {
+ // A partition spec gets min(ceil(size of its manifests / target
size), 200) clusters, as in Trino's
+ // optimize_manifests. Four small manifests fit in one target-size
manifest, so they become one.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ PartitionSpec spec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").identity("name")
+ .build();
+ ActionTestTables.createTable(catalog, "t", spec);
+ append(catalog, partitionedFile(spec, "f1.parquet", 1, "a"));
+ append(catalog, partitionedFile(spec, "f2.parquet", 1, "b"));
+ append(catalog, partitionedFile(spec, "f3.parquet", 2, "a"));
+ append(catalog, partitionedFile(spec, "f4.parquet", 2, "b"));
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("4", "1"),
result.getRows().get(0));
+ Assertions.assertEquals(Collections.singletonList(new
HashSet<>(Arrays.asList(1, 2))),
+ firstPartitionValuesPerManifest(catalog));
+ }
+
+ @Test
+ public void clustersAdjacentFirstPartitionValues() {
+ // With two clusters, the sorted values 1, 7, 50 and 100 go to runs
{1, 7} and {50, 100}, so each manifest
+ // covers a narrow range of the first partition field. The files are
appended out of order, and a HashSet
+ // of these Integers iterates 1, 50, 100, 7, so clustering the values
unsorted would mix the runs.
+ // MUTATION: not sorting the values -> red.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ PartitionSpec spec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").identity("name")
+ .build();
+ ActionTestTables.createTable(catalog, "t", spec);
+ append(catalog, partitionedFile(spec, "f1.parquet", 100, "a"));
+ append(catalog, partitionedFile(spec, "f2.parquet", 1, "b"));
+ append(catalog, partitionedFile(spec, "f3.parquet", 50, "a"));
+ append(catalog, partitionedFile(spec, "f4.parquet", 7, "b"));
+ targetClusters(catalog, 2);
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("4", "2"),
result.getRows().get(0));
+ Assertions.assertEquals(
+ Arrays.asList(new HashSet<>(Arrays.asList(1, 7)), new
HashSet<>(Arrays.asList(50, 100))),
+ firstPartitionValuesPerManifest(catalog));
+ }
+
+ @Test
+ public void clustersPartitionsReadFromTheSameManifest() {
+ // One commit can write several partitions into one manifest: each
commit below writes both, and with two
+ // clusters the rewrite splits them by partition value.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ PartitionSpec spec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").build();
+ ActionTestTables.createTable(catalog, "t", spec);
+ append(catalog, partitionedFile(spec, "f1.parquet", 1),
partitionedFile(spec, "f2.parquet", 2));
+ append(catalog, partitionedFile(spec, "f3.parquet", 1),
partitionedFile(spec, "f4.parquet", 2));
+ targetClusters(catalog, 2);
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("2", "2"),
result.getRows().get(0));
+ assertOneFirstPartitionValuePerManifest(catalog, Arrays.asList(1, 2));
+ }
+
+ @Test
+ public void clustersANullFirstPartitionValue() {
+ // A NULL partition value sorts first, so with two clusters it takes
the first and 1 the second. Iceberg
+ // rejects a null cluster key, and the cluster of a NULL value is a
number like any other.
+ // MUTATION: sorting without NULLs first -> red (the comparator does
not take a null).
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ PartitionSpec spec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").identity("name")
+ .build();
+ ActionTestTables.createTable(catalog, "t", spec);
+ append(catalog, partitionedFile(spec, "f1.parquet", null, "a"));
+ append(catalog, partitionedFile(spec, "f2.parquet", null, "b"));
+ append(catalog, partitionedFile(spec, "f3.parquet", 1, "a"));
+ targetClusters(catalog, 2);
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("3", "2"),
result.getRows().get(0));
+ assertOneFirstPartitionValuePerManifest(catalog, Arrays.asList(null,
1));
+ }
+
+ @Test
+ public void clustersEachPartitionSpecByItsOwnFirstField() {
+ // After the spec changed from identity(id) to identity(name), the
manifests of each spec are clustered by
+ // that spec's own first field, an int in one and a string in the
other: one comparator for all the values
+ // would fail on the mix. The files are fast-appended, since a merge
append would also merge the manifests
+ // of the old spec. MUTATION: clustering every spec with the current
spec's first field -> red.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ TableIdentifier id = ActionTestTables.id("t");
+ PartitionSpec idSpec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").build();
+ ActionTestTables.createTable(catalog, "t", idSpec);
+
catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(idSpec,
"f1.parquet", 1)).commit();
+
catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(idSpec,
"f2.parquet", 2)).commit();
+
catalog.loadTable(id).updateSpec().removeField("id").addField("name").commit();
+ PartitionSpec nameSpec = catalog.loadTable(id).spec();
+
catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(nameSpec,
"f3.parquet", "a")).commit();
+
catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(nameSpec,
"f4.parquet", "b")).commit();
+ targetClusters(catalog, 4);
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("4", "4"),
result.getRows().get(0));
+ assertOneFirstPartitionValuePerManifest(catalog, Arrays.asList(1, 2,
"a", "b"));
+ }
+
+ @Test
+ public void
failsBeforeCommitWhenTheFirstPartitionFieldIsPromotedDuringTheRewrite() {
+ // Another writer promotes the identity source column from INT to LONG
after the table was loaded. Iceberg
+ // rebuilds the existing specs with the new type under the same ids,
so the rewrite reads the targeted
+ // manifests' values as Longs while the values read ahead were
Integers. The rewrite fails before it
+ // commits, and the table is left as it was.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ TableIdentifier id = ActionTestTables.id("t");
+ PartitionSpec spec =
PartitionSpec.builderFor(ActionTestTables.SCHEMA).identity("id").build();
+ ActionTestTables.createTable(catalog, "t", spec);
+ catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(spec,
"f1.parquet", 1)).commit();
+ catalog.loadTable(id).newFastAppend().appendFile(partitionedFile(spec,
"f2.parquet", 2)).commit();
+ targetClusters(catalog, 2);
+ Table loaded = catalog.loadTable(id);
+ catalog.loadTable(id).updateSchema().updateColumn("id",
org.apache.iceberg.types.Types.LongType.get())
+ .commit();
+ Set<String> manifestsBefore = manifestPaths(catalog.loadTable(id));
+
+ DorisConnectorException e =
Assertions.assertThrows(DorisConnectorException.class, () -> rewrite(loaded));
+
+ Assertions.assertTrue(e.getMessage().contains("the table's
partitioning changed during the rewrite"),
+ e.getMessage());
+ Table after = catalog.loadTable(id);
+ Assertions.assertEquals(loaded.currentSnapshot().snapshotId(),
after.currentSnapshot().snapshotId());
+ Assertions.assertEquals(manifestsBefore, manifestPaths(after));
+ }
+
+ @Test
+ public void clusterCountIsTheManifestSizeOverTheTargetUpTo200() {
+ // As in Trino, at most 200 clusters, and so at most 200 open manifest
writers, per spec; the division
+ // rounds up without overflowing. MUTATION: no cap, or rounding up as
(size + target - 1) / target -> red.
+ long target = 8L * 1024 * 1024;
+ Assertions.assertEquals(1, RewriteManifestExecutor.clusterCount(1,
target));
+ Assertions.assertEquals(1,
RewriteManifestExecutor.clusterCount(target, target));
+ Assertions.assertEquals(2, RewriteManifestExecutor.clusterCount(target
+ 1, target));
+ Assertions.assertEquals(200, RewriteManifestExecutor.clusterCount(200
* target, target));
+ Assertions.assertEquals(200,
RewriteManifestExecutor.clusterCount(Long.MAX_VALUE, target));
+ Assertions.assertEquals(3,
RewriteManifestExecutor.clusterCount(Long.MAX_VALUE, Long.MAX_VALUE / 2));
+ }
+
+ @Test
+ public void assignsSortedValuesToRunsOfNearlyEqualLength() {
+ // 1000 values in 200 clusters: five adjacent values per cluster, NULL
first. Three values in 200 clusters
+ // are three clusters.
+ Comparator<Object> order =
Comparators.<Object>nullsFirst().thenComparing(
+
Comparators.forType(org.apache.iceberg.types.Types.IntegerType.get()));
+ Set<Object> values = new HashSet<>();
+ values.add(null);
+ for (int i = 0; i < 999; i++) {
+ values.add(i);
+ }
+
+ Map<Object, Integer> clusters =
RewriteManifestExecutor.assignClusters(values, order, 200);
+
+ Assertions.assertEquals(0, clusters.get(null).intValue());
+ int[] sizes = new int[200];
+ int previous = clusters.get(null);
+ sizes[previous]++;
+ for (int i = 0; i < 999; i++) {
+ int cluster = clusters.get(i);
+ Assertions.assertTrue(cluster >= previous, "cluster numbers follow
the sort order");
+ sizes[cluster]++;
+ previous = cluster;
+ }
+ for (int size : sizes) {
+ Assertions.assertEquals(5, size);
+ }
+ Assertions.assertEquals(3, new
HashSet<>(RewriteManifestExecutor.assignClusters(
+ new HashSet<>(Arrays.asList(3, 1, 2)), order,
200).values()).size());
+ }
+
+ @Test
+ public void leavesASingleSmallManifestUntouched() {
+ // One manifest below commit.manifest.target-size-bytes has nothing to
merge with, as in Spark's and
+ // Trino's manifest rewrites: nothing is committed and nothing is
counted.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ TableIdentifier id = ActionTestTables.id("t");
+ ActionTestTables.createTable(catalog, "t");
+ ActionTestTables.appendSnapshot(catalog, "t", "f1.parquet", 1L);
+ long snapshotBefore =
catalog.loadTable(id).currentSnapshot().snapshotId();
+
+ ConnectorProcedureResult result = rewriteAll(catalog);
+
+ Assertions.assertEquals(ImmutableList.of("0", "0"),
result.getRows().get(0));
+ Assertions.assertEquals(snapshotBefore,
catalog.loadTable(id).currentSnapshot().snapshotId(),
+ "no snapshot is committed");
+ }
+
+ @Test
+ public void keepsAManifestCommittedAfterTheTableWasLoaded() {
+ // The rewrite targets the manifests of the snapshot the table was
loaded with. A manifest that another
+ // writer commits before the rewrite applies is kept as it is, and the
counts are those of the snapshot
+ // the rewrite committed, which the table's manifests before and after
it would not give.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ TableIdentifier id = ActionTestTables.id("t");
+ ActionTestTables.createTable(catalog, "t");
+ ActionTestTables.appendSnapshot(catalog, "t", "f1.parquet", 1L);
+ ActionTestTables.appendSnapshot(catalog, "t", "f2.parquet", 2L);
+ Table loaded = catalog.loadTable(id);
+ ActionTestTables.appendSnapshot(catalog, "t", "f3.parquet", 3L);
+ Set<String> concurrentManifests = manifestPaths(catalog.loadTable(id));
+ concurrentManifests.removeAll(manifestPaths(loaded));
+ Assertions.assertEquals(1, concurrentManifests.size());
+
+ ConnectorProcedureResult result = rewrite(loaded);
+
+ Assertions.assertEquals(ImmutableList.of("2", "1"),
result.getRows().get(0));
+ Table after = catalog.loadTable(id);
+ Set<String> manifestsAfter = manifestPaths(after);
+ Assertions.assertEquals(2, manifestsAfter.size(),
manifestsAfter.toString());
+ Assertions.assertTrue(manifestsAfter.containsAll(concurrentManifests),
manifestsAfter.toString());
+ Assertions.assertEquals(3,
Iterables.size(after.newScan().planFiles()), "every data file is kept");
+ }
+
+ @Test
+ public void keepsAManifestOfAPartitionSpecAddedAfterTheTableWasLoaded() {
+ // Iceberg's RewriteManifests takes the partition specs of the table
it is created from, so it cannot
+ // rewrite a manifest of a spec that another writer adds after the
table was loaded: a rewrite that
+ // reached it failed with a NullPointerException. That manifest is
kept as it is. The other writer fast
+ // appends, because a merge append would also merge the manifests of
the old spec.
+ InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+ TableIdentifier id = ActionTestTables.id("t");
+ ActionTestTables.createTable(catalog, "t");
+ ActionTestTables.appendSnapshot(catalog, "t", "f1.parquet", 1L);
+ ActionTestTables.appendSnapshot(catalog, "t", "f2.parquet", 2L);
+ Table loaded = catalog.loadTable(id);
+ catalog.loadTable(id).updateSpec().addField("id").commit();
+ Table evolved = catalog.loadTable(id);
+ evolved.newFastAppend().appendFile(partitionedFile(evolved.spec(),
"f3.parquet", 3)).commit();
+
+ ConnectorProcedureResult result = rewrite(loaded);
+
+ Assertions.assertEquals(ImmutableList.of("2", "1"),
result.getRows().get(0));
+ Table after = catalog.loadTable(id);
+ List<Integer> specIds = new ArrayList<>();
+ for (ManifestFile manifest :
after.currentSnapshot().dataManifests(after.io())) {
+ specIds.add(manifest.partitionSpecId());
+ }
+ Collections.sort(specIds);
+ Assertions.assertEquals(Arrays.asList(0, 1), specIds);
+ Assertions.assertEquals(3,
Iterables.size(after.newScan().planFiles()), "every data file is kept");
+ }
+
+ private static ConnectorProcedureResult rewriteAll(InMemoryCatalog
catalog) {
+ return rewrite(catalog.loadTable(ActionTestTables.id("t")));
+ }
+
+ private static ConnectorProcedureResult rewrite(Table table) {
+ IcebergRewriteManifestsAction action = action(Collections.emptyMap());
+ action.validate();
+ return action.execute(table, ActionTestTables.session("UTC"));
+ }
+
+ private static Set<String> manifestPaths(Table table) {
+ Set<String> paths = new HashSet<>();
+ for (ManifestFile manifest :
table.currentSnapshot().dataManifests(table.io())) {
+ paths.add(manifest.path());
+ }
+ return paths;
+ }
+
+ private static DataFile partitionedFile(PartitionSpec spec, String
fileName, Object... partitionValues) {
+ PartitionData partition = new PartitionData(spec.partitionType());
+ for (int i = 0; i < partitionValues.length; i++) {
+ partition.set(i, partitionValues[i]);
+ }
+ return DataFiles.builder(spec)
+ .withPath("s3://b/db1/" + fileName)
+ .withFileSizeInBytes(1024)
+ .withRecordCount(1)
+ .withFormat(FileFormat.PARQUET)
+ .withPartition(partition)
+ .build();
+ }
+
+ private static void append(InMemoryCatalog catalog, DataFile... files) {
+ AppendFiles append =
catalog.loadTable(ActionTestTables.id("t")).newAppend();
+ for (DataFile file : files) {
+ append.appendFile(file);
+ }
+ append.commit();
+ }
+
+ /**
+ * Sets the target manifest size so that t's data manifests make the given
number of clusters: each manifest
+ * is a few KB, so ceil(total / ceil(total / clusters)) is clusters, and a
rewritten manifest, which holds one
+ * header for the files of several, stays below the target.
+ */
+ private static void targetClusters(InMemoryCatalog catalog, int clusters) {
+ Table table = catalog.loadTable(ActionTestTables.id("t"));
+ long total = 0;
+ for (ManifestFile manifest :
table.currentSnapshot().dataManifests(table.io())) {
+ total += manifest.length();
+ }
+ table.updateProperties()
+ .set(TableProperties.MANIFEST_TARGET_SIZE_BYTES,
String.valueOf((total + clusters - 1) / clusters))
+ .commit();
+ }
+
+ /** The first-partition-field values of each data manifest of t, ordered
by the manifests' smallest values. */
+ private static List<Set<Object>>
firstPartitionValuesPerManifest(InMemoryCatalog catalog) {
+ Table table = catalog.loadTable(ActionTestTables.id("t"));
+ List<Set<Object>> manifests = new ArrayList<>();
+ for (ManifestFile manifest :
table.currentSnapshot().dataManifests(table.io())) {
+ Set<Object> manifestValues = new HashSet<>();
+ try (CloseableIterable<DataFile> files =
ManifestFiles.read(manifest, table.io(), table.specs())) {
+ for (DataFile file : files) {
+ manifestValues.add(file.partition().get(0, Object.class));
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ manifests.add(manifestValues);
+ }
+ manifests.sort(Comparator.comparingInt((Set<Object> values) ->
values.stream()
+ .mapToInt(value -> (Integer) value).min().getAsInt()));
+ return manifests;
+ }
+
+ /** Every data manifest holds files of one first-partition-field value,
and together they hold expected. */
+ private static void
assertOneFirstPartitionValuePerManifest(InMemoryCatalog catalog, List<?>
expected) {
+ Table table = catalog.loadTable(ActionTestTables.id("t"));
+ List<Object> values = new ArrayList<>();
+ for (ManifestFile manifest :
table.currentSnapshot().dataManifests(table.io())) {
+ Set<Object> manifestValues = new HashSet<>();
+ try (CloseableIterable<DataFile> files =
ManifestFiles.read(manifest, table.io(), table.specs())) {
+ for (DataFile file : files) {
+ manifestValues.add(file.partition().get(0, Object.class));
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ Assertions.assertEquals(1, manifestValues.size(), manifest.path()
+ " holds " + manifestValues);
+ values.addAll(manifestValues);
+ }
+ Assertions.assertEquals(new HashSet<>(expected), new
HashSet<>(values));
+ Assertions.assertEquals(expected.size(), values.size(), "one manifest
per first partition value");
}
@Test
diff --git
a/regression-test/data/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.out
b/regression-test/data/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.out
index 0d68ae09dc0..e97ac9dec80 100644
---
a/regression-test/data/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.out
+++
b/regression-test/data/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.out
@@ -35,6 +35,20 @@
5 item5 500 2024 3
6 item6 600 2024 3
+-- !manifests_by_spec_before --
+0 1
+1 2
+
+-- !spec_id_rewrite --
+2 1
+
+-- !manifests_by_spec_after --
+0 1
+1 1
+
+-- !single_manifest_spec_rewrite --
+0 0
+
-- !after_spec_id_rewrite --
1 item1 100 2024 1 15
2 item2 200 2024 1 16
diff --git
a/regression-test/suites/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.groovy
b/regression-test/suites/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.groovy
index 8adb3161066..075b204f232 100644
---
a/regression-test/suites/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.groovy
+++
b/regression-test/suites/external_table_p0/iceberg/action/test_iceberg_rewrite_manifests.groovy
@@ -238,30 +238,28 @@ suite("test_iceberg_rewrite_manifests", "p0,external") {
sql """INSERT INTO ${db_name}.${spec_id_table} VALUES (4, 'item4', 400,
2024, 3, 18)"""
sql """INSERT INTO ${db_name}.${spec_id_table} VALUES (5, 'item5', 500,
2024, 3, 19)"""
- // Check spec_ids after adding new partition field
- List<List<Object>> allSpecs = sql """
- SELECT partition_spec_id, COUNT(*) as manifest_count
- FROM ${spec_id_table}\$manifests
- GROUP BY partition_spec_id
- ORDER BY partition_spec_id
+ // The first INSERT after the spec change merges the three spec-0
manifests into one, because Iceberg's
+ // merge append compacts the manifests of a spec that the commit does not
write to. So spec 0 has one
+ // manifest and spec 1 has two.
+ order_qt_manifests_by_spec_before """
+ SELECT partition_spec_id, COUNT(*) FROM ${spec_id_table}\$manifests
GROUP BY partition_spec_id
"""
- logger.info("All spec IDs and manifest counts: ${allSpecs}")
-
- if (allSpecs.size() > 0) {
- int targetSpec = allSpecs[0][0] as int
- int targetCount = allSpecs[0][1] as int
- List<List<Object>> specResult = sql """
- ALTER TABLE ${catalog_name}.${db_name}.${spec_id_table}
- EXECUTE rewrite_manifests('spec_id' = '${targetSpec}')
- """
- int specRewritten = specResult[0][0] as int
- assertTrue(specRewritten == targetCount,
- "Should rewrite exactly ${targetCount} manifests for
spec_id=${targetSpec}, got ${specRewritten}")
- qt_after_spec_id_rewrite """SELECT * FROM ${spec_id_table} ORDER BY
id"""
- logger.info("spec_id filtering test completed successfully")
- } else {
- logger.warn("Could not create spec_id, skipping spec_id filtering
test")
- }
+
+ // spec_id limits the rewrite to the two manifests of spec 1, which are
merged into one
+ qt_spec_id_rewrite """
+ ALTER TABLE ${catalog_name}.${db_name}.${spec_id_table}
+ EXECUTE rewrite_manifests('spec_id' = '1')
+ """
+ order_qt_manifests_by_spec_after """
+ SELECT partition_spec_id, COUNT(*) FROM ${spec_id_table}\$manifests
GROUP BY partition_spec_id
+ """
+
+ // A single manifest smaller than the target size is left alone, as
Spark's RewriteManifests action does
+ qt_single_manifest_spec_rewrite """
+ ALTER TABLE ${catalog_name}.${db_name}.${spec_id_table}
+ EXECUTE rewrite_manifests('spec_id' = '0')
+ """
+ qt_after_spec_id_rewrite """SELECT * FROM ${spec_id_table} ORDER BY id"""
//
=====================================================================================
// Test Case 4: rewrite_manifests on empty table (no current snapshot)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]