This is an automated email from the ASF dual-hosted git repository.

amogh-jahagirdar 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 9719db6d61 Core: V4 refactor metrics config (#18269)
9719db6d61 is described below

commit 9719db6d616ba9e2c2d922071ef8c148b9f81849
Author: Ryan Blue <[email protected]>
AuthorDate: Mon Sep 28 16:32:39 2026 -0700

    Core: V4 refactor metrics config (#18269)
---
 .../java/org/apache/iceberg/MetricsConfig.java     | 130 ++++++++++++++-------
 .../main/java/org/apache/iceberg/MetricsUtil.java  |  14 ++-
 .../java/org/apache/iceberg/MetricsTestUtil.java   |  39 +++++++
 .../org/apache/iceberg/TestFileGenerationUtil.java |   9 +-
 .../test/java/org/apache/iceberg/TestMetrics.java  |  24 ++--
 .../java/org/apache/iceberg/TestMetricsConfig.java |   8 +-
 .../java/org/apache/iceberg/TestMetricsModes.java  |  12 +-
 .../java/org/apache/iceberg/TestStatsUtil.java     |   8 +-
 .../org/apache/iceberg/TestV4ManifestReader.java   |  17 ++-
 .../java/org/apache/iceberg/orc/OrcMetrics.java    |   3 +-
 .../org/apache/iceberg/parquet/ParquetMetrics.java |  16 +--
 .../org/apache/iceberg/parquet/TestParquet.java    |  10 +-
 .../apache/iceberg/parquet/TestVariantMetrics.java |  31 +++--
 .../extensions/TestMigrateTableProcedure.java      |   2 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  19 ++-
 .../extensions/TestMigrateTableProcedure.java      |   2 +-
 .../apache/iceberg/spark/source/SparkBatch.java    |   4 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  19 ++-
 .../extensions/TestMigrateTableProcedure.java      |   2 +-
 .../apache/iceberg/spark/source/SparkBatch.java    |   4 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  19 ++-
 .../extensions/TestMigrateTableProcedure.java      |   2 +-
 .../apache/iceberg/spark/source/SparkBatch.java    |   4 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  19 ++-
 24 files changed, 273 insertions(+), 144 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/MetricsConfig.java 
b/core/src/main/java/org/apache/iceberg/MetricsConfig.java
index 87dae4c95d..273d51f888 100644
--- a/core/src/main/java/org/apache/iceberg/MetricsConfig.java
+++ b/core/src/main/java/org/apache/iceberg/MetricsConfig.java
@@ -253,79 +253,119 @@ public final class MetricsConfig implements Serializable 
{
    * @param schema table schema
    * @param order sort order columns, will be promoted to truncate(16)
    * @return metrics configuration
+   * @deprecated will be removed in 1.14.0; use {@link #forTable(Table)} 
instead.
    */
+  @Deprecated
   public static MetricsConfig from(Map<String, String> props, Schema schema, 
SortOrder order) {
-    int maxInferredDefaultColumns = maxInferredColumnDefaults(props);
-    Map<Integer, String> idToName = Maps.newHashMap();
-    Map<String, MetricsMode> columnModes = Maps.newHashMap();
+    int maxDefaultColumns = maxInferredColumnDefaults(props);
 
-    // Handle user override of default mode
-    MetricsMode defaultMode;
-    String configuredDefault = props.get(DEFAULT_WRITE_METRICS_MODE);
+    // Handle configured default mode
+    MetricsMode configuredDefault = configuredDefault(props);
+    Map<String, MetricsMode> defaultColumnConf = defaultColumnModes(schema, 
maxDefaultColumns);
 
+    MetricsMode defaultMode;
     if (configuredDefault != null) {
-      // a user-configured default mode is applied for all columns
-      defaultMode = parseMode(configuredDefault, DEFAULT_MODE, "default");
-    } else if (schema == null) {
+      defaultMode = configuredDefault;
+    } else if (defaultColumnConf.size() < maxDefaultColumns) {
+      // an additional column should use the default mode
       defaultMode = DEFAULT_MODE;
     } else {
-      Set<Integer> ids = TypeUtil.getProjectedIds(schema);
-      if (ids.size() <= maxInferredDefaultColumns) {
-        for (int id : ids) {
-          idToName.put(id, schema.findColumnName(id));
-        }
+      // an additional column should not store metrics
+      defaultMode = MetricsModes.None.get();
+    }
 
-        // there are less than the inferred limit (including structs), so the 
default is used
-        // everywhere
-        defaultMode = DEFAULT_MODE;
-      } else {
-        for (Integer id : limitFieldIds(schema, maxInferredDefaultColumns)) {
-          String name = schema.findColumnName(id);
-          idToName.put(id, name);
-          columnModes.put(name, DEFAULT_MODE);
-        }
+    Map<String, MetricsMode> columnModes = Maps.newHashMap();
+
+    if (configuredDefault == null) {
+      columnModes.putAll(defaultColumnConf);
+    }
+
+    // Default sort columns to at least truncate (overridden by config)
+    columnModes.putAll(sortColumnModes(order, configuredDefault));
+
+    // Override automatic modes with configured modes
+    columnModes.putAll(configuredColumnModes(props));
+
+    Map<Integer, String> idToName = idToName(schema, columnModes);
+
+    return new MetricsConfig(columnModes, defaultMode, idToName);
+  }
+
+  private static MetricsMode configuredDefault(Map<String, String> props) {
+    String configuredDefault = props.get(DEFAULT_WRITE_METRICS_MODE);
+    if (configuredDefault != null) {
+      // a user-configured default mode is applied for all columns
+      return parseMode(configuredDefault, null, "default");
+    }
+
+    return null;
+  }
 
-        // all other columns don't use metrics
-        defaultMode = MetricsModes.None.get();
+  private static Map<String, MetricsMode> defaultColumnModes(Schema schema, 
int maxColumns) {
+    ImmutableMap.Builder<String, MetricsMode> builder = ImmutableMap.builder();
+    if (schema != null) {
+      for (int id : limitFieldIds(schema, maxColumns)) {
+        builder.put(schema.findColumnName(id), DEFAULT_MODE);
       }
     }
 
-    // First set sorted column with sorted column default (can be overridden 
by user)
-    MetricsMode sortedColDefaultMode = sortedColumnDefaultMode(defaultMode);
-    Set<String> sortedCols = SortOrderUtil.orderPreservingSortedColumns(order);
-    sortedCols.forEach(
-        name -> {
-          columnModes.put(name, sortedColDefaultMode);
-          Types.NestedField field = schema != null ? schema.findField(name) : 
null;
-          if (field != null) {
-            idToName.put(field.fieldId(), name);
-          }
-        });
+    return builder.build();
+  }
+
+  private static Map<String, MetricsMode> sortColumnModes(
+      SortOrder order, MetricsMode configuredDefault) {
+    ImmutableMap.Builder<String, MetricsMode> builder = ImmutableMap.builder();
+    MetricsMode sortDefault = promoteToIncludeBounds(configuredDefault);
+    for (String name : SortOrderUtil.orderPreservingSortedColumns(order)) {
+      builder.put(name, sortDefault);
+    }
 
-    // Handle user overrides of defaults
+    return builder.build();
+  }
+
+  private static Map<String, MetricsMode> configuredColumnModes(Map<String, 
String> props) {
+    ImmutableMap.Builder<String, MetricsMode> builder = ImmutableMap.builder();
     for (String key : props.keySet()) {
       if (key.startsWith(METRICS_MODE_COLUMN_CONF_PREFIX)) {
         String columnAlias = key.replaceFirst(METRICS_MODE_COLUMN_CONF_PREFIX, 
"");
-        MetricsMode mode = parseMode(props.get(key), defaultMode, "column " + 
columnAlias);
-        columnModes.put(columnAlias, mode);
-        Types.NestedField field = schema != null ? 
schema.findField(columnAlias) : null;
+        MetricsMode mode = parseMode(props.get(key), null, "column " + 
columnAlias);
+        if (mode != null) {
+          builder.put(columnAlias, mode);
+        }
+      }
+    }
+
+    return builder.build();
+  }
+
+  private static Map<Integer, String> idToName(
+      Schema schema, Map<String, MetricsMode> columnModes) {
+    if (schema != null) {
+      ImmutableMap.Builder<Integer, String> builder = ImmutableMap.builder();
+      for (String name : columnModes.keySet()) {
+        Types.NestedField field = schema.findField(name);
         if (field != null) {
-          idToName.put(field.fieldId(), columnAlias);
+          builder.put(field.fieldId(), name);
         }
       }
+
+      return builder.buildKeepingLast();
     }
 
-    return new MetricsConfig(columnModes, defaultMode, idToName);
+    return null;
   }
 
   /**
-   * Auto promote sorted columns to truncate(16) if default is set at Counts 
or None.
+   * Mode used for automatic metrics for sort and partitioning. Uses 
truncate(16) if default is
+   * Counts or None.
    *
    * @param defaultMode default mode
    * @return mode to use
    */
-  private static MetricsMode sortedColumnDefaultMode(MetricsMode defaultMode) {
-    if (defaultMode == MetricsModes.None.get() || defaultMode == 
MetricsModes.Counts.get()) {
+  private static MetricsMode promoteToIncludeBounds(MetricsMode defaultMode) {
+    if (defaultMode == null
+        || defaultMode == MetricsModes.None.get()
+        || defaultMode == MetricsModes.Counts.get()) {
       return MetricsModes.Truncate.withLength(16);
     } else {
       return defaultMode;
diff --git a/core/src/main/java/org/apache/iceberg/MetricsUtil.java 
b/core/src/main/java/org/apache/iceberg/MetricsUtil.java
index 5fa6c95a85..c93523aed4 100644
--- a/core/src/main/java/org/apache/iceberg/MetricsUtil.java
+++ b/core/src/main/java/org/apache/iceberg/MetricsUtil.java
@@ -112,9 +112,7 @@ public class MetricsUtil {
 
     return fieldMetrics
         .filter(metrics -> !inMapOrList(inputSchema, parents, metrics.id()))
-        .filter(
-            metrics ->
-                metricsMode(inputSchema, metricsConfig, metrics.id()) != 
MetricsModes.None.get())
+        .filter(metrics -> metricsConfig.columnMode(metrics.id()) != 
MetricsModes.None.get())
         .collect(Collectors.toMap(FieldMetrics::id, 
FieldMetrics::nanValueCount));
   }
 
@@ -129,14 +127,18 @@ public class MetricsUtil {
     return false;
   }
 
-  /** Extract MetricsMode for the given field id from metrics config. */
+  /**
+   * Extract MetricsMode for the given field id from metrics config.
+   *
+   * @deprecated will be removed in 1.14.0; use metricsConfig.columnMode(int) 
instead.
+   */
+  @Deprecated
   public static MetricsModes.MetricsMode metricsMode(
       Schema inputSchema, MetricsConfig metricsConfig, int fieldId) {
     Preconditions.checkNotNull(inputSchema, "inputSchema is required");
     Preconditions.checkNotNull(metricsConfig, "metricsConfig is required");
 
-    String columnName = inputSchema.findColumnName(fieldId);
-    return metricsConfig.columnMode(columnName);
+    return metricsConfig.columnMode(fieldId);
   }
 
   public static final List<ReadableMetricColDefinition> READABLE_METRIC_COLS =
diff --git a/core/src/test/java/org/apache/iceberg/MetricsTestUtil.java 
b/core/src/test/java/org/apache/iceberg/MetricsTestUtil.java
new file mode 100644
index 0000000000..db0874d111
--- /dev/null
+++ b/core/src/test/java/org/apache/iceberg/MetricsTestUtil.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import java.util.Map;
+import org.mockito.Mockito;
+
+public class MetricsTestUtil {
+  private MetricsTestUtil() {}
+
+  public static MetricsConfig from(Map<String, String> properties, Schema 
schema) {
+    return from(properties, schema, null);
+  }
+
+  public static MetricsConfig from(
+      Map<String, String> properties, Schema schema, SortOrder sortOrder) {
+    Table mock = Mockito.mock(Table.class);
+    Mockito.when(mock.properties()).thenReturn(properties);
+    Mockito.when(mock.schema()).thenReturn(schema);
+    Mockito.when(mock.sortOrder()).thenReturn(sortOrder);
+    return MetricsConfig.forTable(mock);
+  }
+}
diff --git a/core/src/test/java/org/apache/iceberg/TestFileGenerationUtil.java 
b/core/src/test/java/org/apache/iceberg/TestFileGenerationUtil.java
index 590d4e1601..1f510be74e 100644
--- a/core/src/test/java/org/apache/iceberg/TestFileGenerationUtil.java
+++ b/core/src/test/java/org/apache/iceberg/TestFileGenerationUtil.java
@@ -106,8 +106,8 @@ public class TestFileGenerationUtil {
   @ValueSource(strings = {"none", "counts", "truncate(16)", "full"})
   void testBoundsForAllMetricsModes(String metricsMode) {
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
-            ImmutableMap.of(TableProperties.DEFAULT_WRITE_METRICS_MODE, 
metricsMode), SCHEMA, null);
+        MetricsTestUtil.from(
+            ImmutableMap.of(TableProperties.DEFAULT_WRITE_METRICS_MODE, 
metricsMode), SCHEMA);
     Metrics metrics =
         FileGenerationUtil.generateRandomMetrics(
             SCHEMA,
@@ -122,10 +122,9 @@ public class TestFileGenerationUtil {
   @ValueSource(strings = {"none", "counts", "truncate(16)", "full"})
   public void testBoundsForNestedTypes(String metricsMode) {
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.DEFAULT_WRITE_METRICS_MODE, 
metricsMode),
-            NESTED_SCHEMA,
-            null);
+            NESTED_SCHEMA);
     Metrics metrics =
         FileGenerationUtil.generateRandomMetrics(
             NESTED_SCHEMA,
diff --git a/core/src/test/java/org/apache/iceberg/TestMetrics.java 
b/core/src/test/java/org/apache/iceberg/TestMetrics.java
index 9b25825316..89d528c22e 100644
--- a/core/src/test/java/org/apache/iceberg/TestMetrics.java
+++ b/core/src/test/java/org/apache/iceberg/TestMetrics.java
@@ -353,7 +353,7 @@ public abstract class TestMetrics {
             MetricsModes.None.get().toString(),
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"nestedStructCol.longCol",
             MetricsModes.Full.get().toString());
-    MetricsConfig config = MetricsConfig.from(properties, NESTED_SCHEMA, null);
+    MetricsConfig config = MetricsTestUtil.from(properties, NESTED_SCHEMA);
 
     Metrics metrics = getMetrics(NESTED_SCHEMA, config, 
buildNestedTestRecord());
     assertThat(metrics.recordCount()).isEqualTo(1L);
@@ -595,8 +595,8 @@ public abstract class TestMetrics {
     Metrics metrics =
         getMetrics(
             NESTED_SCHEMA,
-            MetricsConfig.from(
-                ImmutableMap.of("write.metadata.metrics.default", "none"), 
NESTED_SCHEMA, null),
+            MetricsTestUtil.from(
+                ImmutableMap.of("write.metadata.metrics.default", "none"), 
NESTED_SCHEMA),
             buildNestedTestRecord());
     assertThat(metrics.recordCount()).isEqualTo(1L);
     assertThat(metrics.columnSizes()).isEmpty();
@@ -617,8 +617,8 @@ public abstract class TestMetrics {
     Metrics metrics =
         getMetrics(
             NESTED_SCHEMA,
-            MetricsConfig.from(
-                ImmutableMap.of("write.metadata.metrics.default", "counts"), 
NESTED_SCHEMA, null),
+            MetricsTestUtil.from(
+                ImmutableMap.of("write.metadata.metrics.default", "counts"), 
NESTED_SCHEMA),
             buildNestedTestRecord());
     assertThat(metrics.recordCount()).isEqualTo(1L);
     assertThat(metrics.columnSizes()).doesNotContainValue(null);
@@ -640,8 +640,8 @@ public abstract class TestMetrics {
     Metrics metrics =
         getMetrics(
             NESTED_SCHEMA,
-            MetricsConfig.from(
-                ImmutableMap.of("write.metadata.metrics.default", "full"), 
NESTED_SCHEMA, null),
+            MetricsTestUtil.from(
+                ImmutableMap.of("write.metadata.metrics.default", "full"), 
NESTED_SCHEMA),
             buildNestedTestRecord());
     assertThat(metrics.recordCount()).isEqualTo(1L);
     assertThat(metrics.columnSizes()).doesNotContainValue(null);
@@ -675,10 +675,9 @@ public abstract class TestMetrics {
     Metrics metrics =
         getMetrics(
             singleStringColSchema,
-            MetricsConfig.from(
+            MetricsTestUtil.from(
                 ImmutableMap.of("write.metadata.metrics.default", 
"truncate(10)"),
-                singleStringColSchema,
-                null),
+                singleStringColSchema),
             record);
 
     CharBuffer expectedMinBound = CharBuffer.wrap("Lorem ipsu");
@@ -702,10 +701,9 @@ public abstract class TestMetrics {
     Metrics metrics =
         getMetrics(
             singleBinaryColSchema,
-            MetricsConfig.from(
+            MetricsTestUtil.from(
                 ImmutableMap.of("write.metadata.metrics.default", 
"truncate(5)"),
-                singleBinaryColSchema,
-                null),
+                singleBinaryColSchema),
             record);
 
     ByteBuffer expectedMinBounds = ByteBuffer.wrap(new byte[] {0x1, 0x2, 0x3, 
0x4, 0x5});
diff --git a/core/src/test/java/org/apache/iceberg/TestMetricsConfig.java 
b/core/src/test/java/org/apache/iceberg/TestMetricsConfig.java
index 4eeada847d..cb4459472c 100644
--- a/core/src/test/java/org/apache/iceberg/TestMetricsConfig.java
+++ b/core/src/test/java/org/apache/iceberg/TestMetricsConfig.java
@@ -128,7 +128,7 @@ public class TestMetricsConfig {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "id", "full",
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "data", "none");
 
-    MetricsConfig config = MetricsConfig.from(props, schema, null);
+    MetricsConfig config = MetricsTestUtil.from(props, schema);
 
     assertThat(config.metricsFieldIds())
         .as("Should track field ids for configured and defaulted columns")
@@ -151,7 +151,7 @@ public class TestMetricsConfig {
             optional(2, "data", Types.StringType.get()),
             optional(3, "category", Types.StringType.get()));
 
-    MetricsConfig config = MetricsConfig.from(ImmutableMap.of(), schema, null);
+    MetricsConfig config = MetricsTestUtil.from(ImmutableMap.of(), schema);
 
     assertThat(config.metricsFieldIds())
         .as("Should track field ids for all columns that use the default mode")
@@ -174,7 +174,7 @@ public class TestMetricsConfig {
 
     Map<String, String> limitedToTwo =
         ImmutableMap.of(TableProperties.METRICS_MAX_INFERRED_COLUMN_DEFAULTS, 
"2");
-    MetricsConfig config = MetricsConfig.from(limitedToTwo, schema, null);
+    MetricsConfig config = MetricsTestUtil.from(limitedToTwo, schema);
 
     // only the fields within the inferred limit are tracked, matching 
limitFieldIds
     assertThat(config.metricsFieldIds())
@@ -195,7 +195,7 @@ public class TestMetricsConfig {
     // raising the limit expands both the tracked ids and the columns with 
metrics
     Map<String, String> limitedToThree =
         ImmutableMap.of(TableProperties.METRICS_MAX_INFERRED_COLUMN_DEFAULTS, 
"3");
-    MetricsConfig wider = MetricsConfig.from(limitedToThree, schema, null);
+    MetricsConfig wider = MetricsTestUtil.from(limitedToThree, schema);
 
     assertThat(wider.metricsFieldIds())
         .as("Raising the limit should track more field ids")
diff --git a/core/src/test/java/org/apache/iceberg/TestMetricsModes.java 
b/core/src/test/java/org/apache/iceberg/TestMetricsModes.java
index f96857f8fb..6c873ec21b 100644
--- a/core/src/test/java/org/apache/iceberg/TestMetricsModes.java
+++ b/core/src/test/java/org/apache/iceberg/TestMetricsModes.java
@@ -87,7 +87,9 @@ public class TestMetricsModes {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col",
             "troncate(5)");
 
-    MetricsConfig config = MetricsConfig.from(properties, null, null);
+    Schema schema = new Schema(required(1, "col", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(properties, schema);
     assertThat(config.columnMode("col"))
         .as("Invalid mode should be defaulted to table default (full)")
         .isEqualTo(MetricsModes.Full.get());
@@ -102,14 +104,16 @@ public class TestMetricsModes {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col",
             "troncate(5)");
 
-    MetricsConfig config = MetricsConfig.from(properties, null, null);
+    Schema schema = new Schema(required(1, "col", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(properties, schema);
     assertThat(config.columnMode("col"))
         .as("Invalid mode should be defaulted to library default 
(truncate(16))")
         .isEqualTo(MetricsModes.Truncate.withLength(16));
   }
 
   @TestTemplate
-  public void testMetricsConfigSortedColsDefault() throws Exception {
+  public void testMetricsConfigSortedColsDefault() {
     Schema schema =
         new Schema(
             required(1, "col1", Types.IntegerType.get()),
@@ -166,7 +170,7 @@ public class TestMetricsModes {
         .isEqualTo(Full.get());
     assertThat(config.columnMode("col2"))
         .as("Original default applies as user entered invalid mode for sorted 
column")
-        .isEqualTo(Counts.get());
+        .isEqualTo(Truncate.withLength(16));
   }
 
   @TestTemplate
diff --git a/core/src/test/java/org/apache/iceberg/TestStatsUtil.java 
b/core/src/test/java/org/apache/iceberg/TestStatsUtil.java
index 71ebf227af..183c4336f7 100644
--- a/core/src/test/java/org/apache/iceberg/TestStatsUtil.java
+++ b/core/src/test/java/org/apache/iceberg/TestStatsUtil.java
@@ -462,7 +462,7 @@ public class TestStatsUtil {
             Types.NestedField.optional(10_400, "data", dataStats));
 
     Types.StructType actual =
-        StatsUtil.statsWriteSchema(schema, 
MetricsConfig.from(ImmutableMap.of(), schema, null));
+        StatsUtil.statsWriteSchema(schema, 
MetricsTestUtil.from(ImmutableMap.of(), schema));
 
     assertSameStructure(expected, actual);
   }
@@ -499,7 +499,7 @@ public class TestStatsUtil {
     Map<String, String> properties =
         ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"data", "none");
     Types.StructType actual =
-        StatsUtil.statsWriteSchema(schema, MetricsConfig.from(properties, 
schema, null));
+        StatsUtil.statsWriteSchema(schema, MetricsTestUtil.from(properties, 
schema));
 
     assertSameStructure(expected, actual);
   }
@@ -541,7 +541,7 @@ public class TestStatsUtil {
     Map<String, String> properties =
         ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"id", "none");
     Types.StructType actual =
-        StatsUtil.statsWriteSchema(schema, MetricsConfig.from(properties, 
schema, null));
+        StatsUtil.statsWriteSchema(schema, MetricsTestUtil.from(properties, 
schema));
 
     assertSameStructure(expected, actual);
   }
@@ -559,7 +559,7 @@ public class TestStatsUtil {
                 Types.MapType.ofOptional(4, 5, Types.StringType.get(), 
Types.StringType.get())));
 
     Types.StructType actual =
-        StatsUtil.statsWriteSchema(schema, 
MetricsConfig.from(ImmutableMap.of(), schema, null));
+        StatsUtil.statsWriteSchema(schema, 
MetricsTestUtil.from(ImmutableMap.of(), schema));
 
     assertThat(actual.fields()).isEmpty();
   }
diff --git a/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java 
b/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
index 92104ac90c..7b8e41a534 100644
--- a/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
+++ b/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
@@ -93,7 +93,7 @@ class TestV4ManifestReader {
       ImmutableMap.of(PartitionSpec.unpartitioned().specId(), 
PartitionSpec.unpartitioned());
 
   private static final MetricsConfig METRICS_CONFIG =
-      MetricsConfig.from(ImmutableMap.of(), TABLE_SCHEMA, null);
+      MetricsTestUtil.from(ImmutableMap.of(), TABLE_SCHEMA);
   private static final Types.StructType STATS_TYPE =
       StatsUtil.statsWriteSchema(TABLE_SCHEMA, METRICS_CONFIG);
   private static final Types.StructType ID_ONLY_STATS_TYPE =
@@ -802,10 +802,9 @@ class TestV4ManifestReader {
   @Test
   public void projectionDependsOnMetricsConfig() {
     MetricsConfig metricsWithoutID =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"id", "none"),
-            TABLE_SCHEMA,
-            null);
+            TABLE_SCHEMA);
 
     Types.StructType readSchema =
         V4ManifestReader.builder(UNREAD_MANIFEST_FILE, IO, TABLE_SCHEMA, 
ID_PARTITIONING_SPECS)
@@ -850,10 +849,9 @@ class TestV4ManifestReader {
   @Test
   public void projectionForScanPlanningOverridesMetricsConfig() {
     MetricsConfig metricsWithoutID =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"id", "none"),
-            TABLE_SCHEMA,
-            null);
+            TABLE_SCHEMA);
 
     Types.StructType readSchema =
         V4ManifestReader.builder(UNREAD_MANIFEST_FILE, IO, TABLE_SCHEMA, 
ID_PARTITIONING_SPECS)
@@ -878,10 +876,9 @@ class TestV4ManifestReader {
   @Test
   public void projectionForScanPlanningIncludesRequestedStatsMetricsConfig() {
     MetricsConfig metricsWithoutID =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"id", "none"),
-            TABLE_SCHEMA,
-            null);
+            TABLE_SCHEMA);
 
     Types.StructType readSchema =
         V4ManifestReader.builder(UNREAD_MANIFEST_FILE, IO, TABLE_SCHEMA, 
ID_PARTITIONING_SPECS)
diff --git a/orc/src/main/java/org/apache/iceberg/orc/OrcMetrics.java 
b/orc/src/main/java/org/apache/iceberg/orc/OrcMetrics.java
index 7498d6e155..6bebb1c3ff 100644
--- a/orc/src/main/java/org/apache/iceberg/orc/OrcMetrics.java
+++ b/orc/src/main/java/org/apache/iceberg/orc/OrcMetrics.java
@@ -163,8 +163,7 @@ public class OrcMetrics {
         final Types.NestedField icebergCol = icebergColOpt.get();
         final int fieldId = icebergCol.fieldId();
 
-        final MetricsMode metricsMode =
-            MetricsUtil.metricsMode(schema, effectiveMetricsConfig, 
icebergCol.fieldId());
+        final MetricsMode metricsMode = 
effectiveMetricsConfig.columnMode(icebergCol.fieldId());
 
         if (metricsMode == MetricsModes.None.get() || inMapOrList(orcCol)) {
           continue;
diff --git 
a/parquet/src/main/java/org/apache/iceberg/parquet/ParquetMetrics.java 
b/parquet/src/main/java/org/apache/iceberg/parquet/ParquetMetrics.java
index 5aed952d9d..346931b375 100644
--- a/parquet/src/main/java/org/apache/iceberg/parquet/ParquetMetrics.java
+++ b/parquet/src/main/java/org/apache/iceberg/parquet/ParquetMetrics.java
@@ -31,7 +31,6 @@ import org.apache.iceberg.FieldMetrics;
 import org.apache.iceberg.Metrics;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
-import org.apache.iceberg.MetricsUtil;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
@@ -93,7 +92,7 @@ class ParquetMetrics {
         fields.collect(Collectors.toMap(FieldMetrics::id, 
Function.identity()));
 
     return TypeWithSchemaVisitor.visit(
-        schema.asStruct(), type, new MetricsVisitor(schema, metricsConfig, 
metricsById, columns));
+        schema.asStruct(), type, new MetricsVisitor(metricsConfig, 
metricsById, columns));
   }
 
   private static long rowCount(ParquetMetadata metadata) {
@@ -106,7 +105,7 @@ class ParquetMetrics {
   }
 
   private static Map<Integer, Long> columnSizes(
-      Schema schema, MessageType type, ParquetMetadata metadata, MetricsConfig 
metricsConfig) {
+      MessageType type, ParquetMetadata metadata, MetricsConfig metricsConfig) 
{
     Map<Integer, Long> columnSizes = Maps.newHashMap();
     for (BlockMetaData block : metadata.getBlocks()) {
       for (ColumnChunkMetaData column : block.getColumns()) {
@@ -114,7 +113,7 @@ class ParquetMetrics {
             
type.getColumnDescription(column.getPath().toArray()).getPrimitiveType().getId();
         if (id != null) {
           int fieldId = id.intValue();
-          MetricsModes.MetricsMode mode = MetricsUtil.metricsMode(schema, 
metricsConfig, fieldId);
+          MetricsModes.MetricsMode mode = metricsConfig.columnMode(fieldId);
           if (mode != MetricsModes.None.get()) {
             columnSizes.put(fieldId, columnSizes.getOrDefault(fieldId, 0L) + 
column.getTotalSize());
           }
@@ -132,7 +131,7 @@ class ParquetMetrics {
       ParquetMetadata metadata,
       Stream<FieldMetrics<?>> fields) {
     long rowCount = rowCount(metadata);
-    Map<Integer, Long> columnSizes = columnSizes(schema, type, metadata, 
metricsConfig);
+    Map<Integer, Long> columnSizes = columnSizes(type, metadata, 
metricsConfig);
 
     Map<Integer, Long> valueCounts = Maps.newHashMap();
     Map<Integer, Long> nullValueCounts = Maps.newHashMap();
@@ -190,17 +189,14 @@ class ParquetMetrics {
   }
 
   private static class MetricsVisitor extends 
TypeWithSchemaVisitor<Iterable<FieldMetrics<?>>> {
-    private final Schema schema;
     private final MetricsConfig metricsConfig;
     private final Map<Integer, FieldMetrics<?>> metricsById;
     private final Multimap<ColumnPath, ColumnChunkMetaData> columns;
 
     private MetricsVisitor(
-        Schema schema,
         MetricsConfig metricsConfig,
         Map<Integer, FieldMetrics<?>> metricsById,
         Multimap<ColumnPath, ColumnChunkMetaData> columns) {
-      this.schema = schema;
       this.metricsConfig = metricsConfig;
       this.metricsById = metricsById;
       this.columns = columns;
@@ -246,7 +242,7 @@ class ParquetMetrics {
       }
       int fieldId = id.intValue();
 
-      MetricsModes.MetricsMode mode = MetricsUtil.metricsMode(schema, 
metricsConfig, fieldId);
+      MetricsModes.MetricsMode mode = metricsConfig.columnMode(fieldId);
       if (mode == MetricsModes.None.get()) {
         return ImmutableList.of();
       }
@@ -397,7 +393,7 @@ class ParquetMetrics {
       }
       int fieldId = id.intValue();
 
-      MetricsModes.MetricsMode mode = MetricsUtil.metricsMode(schema, 
metricsConfig, fieldId);
+      MetricsModes.MetricsMode mode = metricsConfig.columnMode(fieldId);
       if (mode == MetricsModes.None.get()) {
         return ImmutableList.of();
       }
diff --git a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquet.java 
b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquet.java
index 1a5b540504..b778536ae5 100644
--- a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquet.java
+++ b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquet.java
@@ -56,6 +56,7 @@ import org.apache.avro.generic.GenericRecordBuilder;
 import org.apache.iceberg.Files;
 import org.apache.iceberg.Metrics;
 import org.apache.iceberg.MetricsConfig;
+import org.apache.iceberg.MetricsTestUtil;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.avro.AvroSchemaUtil;
@@ -926,10 +927,15 @@ public class TestParquet {
 
   @Test
   public void missingNullCountWithCountsMode() {
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "data", Types.StringType.get()));
+
     Metrics metrics =
         missingNullCountMetrics(
-            MetricsConfig.fromProperties(
-                Collections.singletonMap("write.metadata.metrics.default", 
"counts")),
+            MetricsTestUtil.from(
+                Collections.singletonMap("write.metadata.metrics.default", 
"counts"), schema),
             block(statsWithoutNullCount(1, 10), 10),
             block(statsWithNullCount(20, 30, 1), 10));
 
diff --git 
a/parquet/src/test/java/org/apache/iceberg/parquet/TestVariantMetrics.java 
b/parquet/src/test/java/org/apache/iceberg/parquet/TestVariantMetrics.java
index 15291a1930..3e0c088192 100644
--- a/parquet/src/test/java/org/apache/iceberg/parquet/TestVariantMetrics.java
+++ b/parquet/src/test/java/org/apache/iceberg/parquet/TestVariantMetrics.java
@@ -33,6 +33,7 @@ import java.util.stream.Stream;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.iceberg.Metrics;
 import org.apache.iceberg.MetricsConfig;
+import org.apache.iceberg.MetricsTestUtil;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.data.GenericRecord;
@@ -293,10 +294,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of(ByteBuffer.wrap(BINARY_20_BYTES));
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "truncate(8)"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
@@ -321,10 +321,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of(ByteBuffer.wrap(BINARY_20_BYTES));
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "full"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
@@ -349,10 +348,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of(ByteBuffer.wrap(BINARY_20_BYTES));
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "counts"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
@@ -373,10 +371,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of("iceberg_variant");
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "truncate(8)"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
@@ -401,10 +398,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of("iceberg_variant_full");
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "full"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
@@ -429,10 +425,9 @@ public class TestVariantMetrics {
     VariantValue value = Variants.of("iceberg_variant");
 
     MetricsConfig metricsConfig =
-        MetricsConfig.from(
+        MetricsTestUtil.from(
             ImmutableMap.of(TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + 
"var", "counts"),
-            SCHEMA,
-            null);
+            SCHEMA);
 
     Metrics metrics =
         writeParquetWithMetricsConfig(
diff --git 
a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
 
b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
index 39bf49ce0c..832e5376bf 100644
--- 
a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
+++ 
b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
@@ -151,7 +151,7 @@ public class TestMigrateTableProcedure extends 
ExtensionsTestBase {
 
     assertThatThrownBy(
             () -> {
-              String props = "map('write.metadata.metrics.column.x', 'X')";
+              String props = "map('write.metadata.metrics.column.x', 
'counts')";
               sql("CALL %s.system.migrate('%s', %s)", catalogName, tableName, 
props);
             })
         .isInstanceOf(ValidationException.class)
diff --git 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index b57480a61e..ca37f933c6 100644
--- 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -25,10 +25,13 @@ import java.util.Map;
 import org.apache.iceberg.KryoHelpers;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
+import org.apache.iceberg.MetricsTestUtil;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.SparkTableUtil.SparkPartition;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Test;
 
 public class TestSparkTableUtil {
@@ -65,7 +68,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
@@ -90,7 +99,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
diff --git 
a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
 
b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
index de1322384b..8b6aa6a722 100644
--- 
a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
+++ 
b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
@@ -151,7 +151,7 @@ public class TestMigrateTableProcedure extends 
ExtensionsTestBase {
 
     assertThatThrownBy(
             () -> {
-              String props = "map('write.metadata.metrics.column.x', 'X')";
+              String props = "map('write.metadata.metrics.column.x', 
'counts')";
               sql("CALL %s.system.migrate('%s', %s)", catalogName, tableName, 
props);
             })
         .isInstanceOf(ValidationException.class)
diff --git 
a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
 
b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
index f90423900d..b50d060955 100644
--- 
a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
+++ 
b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
@@ -28,7 +28,6 @@ import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
-import org.apache.iceberg.MetricsUtil;
 import org.apache.iceberg.ScanTask;
 import org.apache.iceberg.ScanTaskGroup;
 import org.apache.iceberg.Schema;
@@ -201,8 +200,7 @@ class SparkBatch implements Batch {
       }
 
       MetricsConfig metricsConfig = MetricsConfig.forTable(table);
-      MetricsModes.MetricsMode mode =
-          MetricsUtil.metricsMode(table.schema(), metricsConfig, 
field.fieldId());
+      MetricsModes.MetricsMode mode = 
metricsConfig.columnMode(field.fieldId());
       if (mode == MetricsModes.None.get() || mode == 
MetricsModes.Counts.get()) {
         return false;
       }
diff --git 
a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
 
b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index b57480a61e..ca37f933c6 100644
--- 
a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ 
b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -25,10 +25,13 @@ import java.util.Map;
 import org.apache.iceberg.KryoHelpers;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
+import org.apache.iceberg.MetricsTestUtil;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.SparkTableUtil.SparkPartition;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Test;
 
 public class TestSparkTableUtil {
@@ -65,7 +68,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
@@ -90,7 +99,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
diff --git 
a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
 
b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
index 5fd0fe9665..80c2fa6e61 100644
--- 
a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
+++ 
b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
@@ -151,7 +151,7 @@ public class TestMigrateTableProcedure extends 
ExtensionsTestBase {
 
     assertThatThrownBy(
             () -> {
-              String props = "map('write.metadata.metrics.column.x', 'X')";
+              String props = "map('write.metadata.metrics.column.x', 
'counts')";
               sql("CALL %s.system.migrate('%s', %s)", catalogName, tableName, 
props);
             })
         .isInstanceOf(ValidationException.class)
diff --git 
a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
 
b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
index 5cafa1c9fa..81fb008963 100644
--- 
a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
+++ 
b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
@@ -28,7 +28,6 @@ import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
-import org.apache.iceberg.MetricsUtil;
 import org.apache.iceberg.ScanTask;
 import org.apache.iceberg.ScanTaskGroup;
 import org.apache.iceberg.Schema;
@@ -211,8 +210,7 @@ class SparkBatch implements Batch {
       }
 
       MetricsConfig metricsConfig = MetricsConfig.forTable(table);
-      MetricsModes.MetricsMode mode =
-          MetricsUtil.metricsMode(table.schema(), metricsConfig, 
field.fieldId());
+      MetricsModes.MetricsMode mode = 
metricsConfig.columnMode(field.fieldId());
       if (mode == MetricsModes.None.get() || mode == 
MetricsModes.Counts.get()) {
         return false;
       }
diff --git 
a/spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
 
b/spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index b57480a61e..ca37f933c6 100644
--- 
a/spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ 
b/spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -25,10 +25,13 @@ import java.util.Map;
 import org.apache.iceberg.KryoHelpers;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
+import org.apache.iceberg.MetricsTestUtil;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.SparkTableUtil.SparkPartition;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Test;
 
 public class TestSparkTableUtil {
@@ -65,7 +68,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
@@ -90,7 +99,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
diff --git 
a/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
 
b/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
index 5fd0fe9665..80c2fa6e61 100644
--- 
a/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
+++ 
b/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMigrateTableProcedure.java
@@ -151,7 +151,7 @@ public class TestMigrateTableProcedure extends 
ExtensionsTestBase {
 
     assertThatThrownBy(
             () -> {
-              String props = "map('write.metadata.metrics.column.x', 'X')";
+              String props = "map('write.metadata.metrics.column.x', 
'counts')";
               sql("CALL %s.system.migrate('%s', %s)", catalogName, tableName, 
props);
             })
         .isInstanceOf(ValidationException.class)
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
index 5cafa1c9fa..81fb008963 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/source/SparkBatch.java
@@ -28,7 +28,6 @@ import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
-import org.apache.iceberg.MetricsUtil;
 import org.apache.iceberg.ScanTask;
 import org.apache.iceberg.ScanTaskGroup;
 import org.apache.iceberg.Schema;
@@ -211,8 +210,7 @@ class SparkBatch implements Batch {
       }
 
       MetricsConfig metricsConfig = MetricsConfig.forTable(table);
-      MetricsModes.MetricsMode mode =
-          MetricsUtil.metricsMode(table.schema(), metricsConfig, 
field.fieldId());
+      MetricsModes.MetricsMode mode = 
metricsConfig.columnMode(field.fieldId());
       if (mode == MetricsModes.None.get() || mode == 
MetricsModes.Counts.get()) {
         return false;
       }
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index b57480a61e..ca37f933c6 100644
--- 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -25,10 +25,13 @@ import java.util.Map;
 import org.apache.iceberg.KryoHelpers;
 import org.apache.iceberg.MetricsConfig;
 import org.apache.iceberg.MetricsModes;
+import org.apache.iceberg.MetricsTestUtil;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.SparkTableUtil.SparkPartition;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Test;
 
 public class TestSparkTableUtil {
@@ -65,7 +68,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))
@@ -90,7 +99,13 @@ public class TestSparkTableUtil {
             TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "col2",
             "truncate(16)");
 
-    MetricsConfig config = MetricsConfig.from(metricsConfig, null, null);
+    Schema schema =
+        new Schema(
+            Types.NestedField.required(1, "col1", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "col2", Types.StringType.get()),
+            Types.NestedField.optional(3, "col3", Types.StringType.get()));
+
+    MetricsConfig config = MetricsTestUtil.from(metricsConfig, schema);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
     assertThat(deserialized.columnMode("col1"))

Reply via email to