This is an automated email from the ASF dual-hosted git repository.
aokolnychyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 6a41168d1a Core: Avoid exceptions for accessing schema for metadata
tables in SnapshotUtil (#15387)
6a41168d1a is described below
commit 6a41168d1aede8597eff057d91681ccd0eafbec1
Author: Anton Okolnychyi <[email protected]>
AuthorDate: Fri Feb 20 14:33:02 2026 -0800
Core: Avoid exceptions for accessing schema for metadata tables in
SnapshotUtil (#15387)
---
.../java/org/apache/iceberg/util/SnapshotUtil.java | 8 +++
.../org/apache/iceberg/util/TestSnapshotUtil.java | 69 ++++++++++++++++++++++
2 files changed, 77 insertions(+)
diff --git a/core/src/main/java/org/apache/iceberg/util/SnapshotUtil.java
b/core/src/main/java/org/apache/iceberg/util/SnapshotUtil.java
index 3c6911abef..dccc23c2e2 100644
--- a/core/src/main/java/org/apache/iceberg/util/SnapshotUtil.java
+++ b/core/src/main/java/org/apache/iceberg/util/SnapshotUtil.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
+import org.apache.iceberg.BaseMetadataTable;
import org.apache.iceberg.DataFile;
import org.apache.iceberg.HistoryEntry;
import org.apache.iceberg.Schema;
@@ -395,11 +396,18 @@ public class SnapshotUtil {
/**
* Returns the schema of the table for the specified snapshot.
*
+ * <p>Note that metadata tables may support time travel but don't inherit
the snapshot schema,
+ * unlike normal data scans.
+ *
* @param table a {@link Table}
* @param snapshotId the ID of the snapshot
* @return the schema
*/
public static Schema schemaFor(Table table, long snapshotId) {
+ if (table instanceof BaseMetadataTable) {
+ return table.schema();
+ }
+
Snapshot snapshot = table.snapshot(snapshotId);
Preconditions.checkArgument(snapshot != null, "Cannot find snapshot with
ID %s", snapshotId);
Integer schemaId = snapshot.schemaId();
diff --git a/core/src/test/java/org/apache/iceberg/util/TestSnapshotUtil.java
b/core/src/test/java/org/apache/iceberg/util/TestSnapshotUtil.java
index 37d0953ab8..96b56e5ffb 100644
--- a/core/src/test/java/org/apache/iceberg/util/TestSnapshotUtil.java
+++ b/core/src/test/java/org/apache/iceberg/util/TestSnapshotUtil.java
@@ -21,6 +21,7 @@ package org.apache.iceberg.util;
import static org.apache.iceberg.types.Types.NestedField.optional;
import static org.apache.iceberg.types.Types.NestedField.required;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.File;
import java.util.Iterator;
@@ -28,10 +29,13 @@ import java.util.List;
import java.util.stream.StreamSupport;
import org.apache.iceberg.DataFile;
import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.MetadataTableType;
+import org.apache.iceberg.MetadataTableUtils;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.Schema;
import org.apache.iceberg.Snapshot;
import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.Table;
import org.apache.iceberg.TestHelpers;
import org.apache.iceberg.TestTables;
import org.apache.iceberg.types.Types;
@@ -248,4 +252,69 @@ public class TestSnapshotUtil {
assertThat(table.schema().asStruct()).isEqualTo(expected.asStruct());
assertThat(SnapshotUtil.schemaFor(table,
tag).asStruct()).isEqualTo(initialSchema.asStruct());
}
+
+ @Test
+ public void schemaForSnapshotId() {
+ Schema initialSchema =
+ new Schema(
+ required(1, "id", Types.IntegerType.get()),
+ required(2, "data", Types.StringType.get()));
+ assertThat(table.schema().asStruct()).isEqualTo(initialSchema.asStruct());
+
+ long firstSnapshotId = table.currentSnapshot().snapshotId();
+ assertThat(SnapshotUtil.schemaFor(table, firstSnapshotId).asStruct())
+ .isEqualTo(initialSchema.asStruct());
+
+ table.updateSchema().addColumn("zip", Types.IntegerType.get()).commit();
+ appendFileToMain();
+
+ Schema updatedSchema =
+ new Schema(
+ required(1, "id", Types.IntegerType.get()),
+ required(2, "data", Types.StringType.get()),
+ optional(3, "zip", Types.IntegerType.get()));
+
+ long secondSnapshotId = table.currentSnapshot().snapshotId();
+
+ assertThat(SnapshotUtil.schemaFor(table, firstSnapshotId).asStruct())
+ .isEqualTo(initialSchema.asStruct());
+ assertThat(SnapshotUtil.schemaFor(table, secondSnapshotId).asStruct())
+ .isEqualTo(updatedSchema.asStruct());
+ }
+
+ @Test
+ public void schemaForSnapshotIdInvalidSnapshot() {
+ long invalidSnapshotId = 999999L;
+ assertThat(table.snapshot(invalidSnapshotId)).isNull();
+
+ assertThatThrownBy(() -> SnapshotUtil.schemaFor(table, invalidSnapshotId))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Cannot find snapshot with ID " +
invalidSnapshotId);
+ }
+
+ @Test
+ public void schemaForSnapshotIdMetadataTable() {
+ long firstSnapshotId = table.currentSnapshot().snapshotId();
+
+ table.updateSchema().addColumn("zip", Types.IntegerType.get()).commit();
+ appendFileToMain();
+
+ long secondSnapshotId = table.currentSnapshot().snapshotId();
+
+ Schema updatedSchema =
+ new Schema(
+ required(1, "id", Types.IntegerType.get()),
+ required(2, "data", Types.StringType.get()),
+ optional(3, "zip", Types.IntegerType.get()));
+
+ assertThat(table.schema().asStruct()).isEqualTo(updatedSchema.asStruct());
+
+ Table snapshotsTable =
+ MetadataTableUtils.createMetadataTableInstance(table,
MetadataTableType.SNAPSHOTS);
+
+ assertThat(SnapshotUtil.schemaFor(snapshotsTable,
secondSnapshotId).asStruct())
+ .isEqualTo(snapshotsTable.schema().asStruct());
+ assertThat(SnapshotUtil.schemaFor(snapshotsTable,
firstSnapshotId).asStruct())
+ .isEqualTo(snapshotsTable.schema().asStruct());
+ }
}