Skip to content
3 changes: 3 additions & 0 deletions .palantir/revapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ acceptedBreaks:
- code: "java.field.removedWithConstant"
old: "field org.apache.iceberg.PartitionStatsHandler.PARTITION_FIELD_NAME"
justification: "Removed deprecated functionality for partition stats"
- code: "java.method.removed"
old: "method org.apache.iceberg.MetricsConfig org.apache.iceberg.MetricsConfig::forPositionDelete(org.apache.iceberg.Table)"
justification: "Deprecated for removal in 1.12.0"
- code: "java.method.removed"
old: "method org.apache.iceberg.Schema org.apache.iceberg.PartitionStatsHandler::schema(org.apache.iceberg.types.Types.StructType,\
\ int)"
Expand Down
128 changes: 81 additions & 47 deletions core/src/main/java/org/apache/iceberg/MetricsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import javax.annotation.concurrent.Immutable;
import org.apache.iceberg.MetricsModes.MetricsMode;
import org.apache.iceberg.exceptions.ValidationException;
import org.apache.iceberg.relocated.com.google.common.base.Joiner;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
Expand All @@ -49,27 +49,37 @@
public final class MetricsConfig implements Serializable {

private static final Logger LOG = LoggerFactory.getLogger(MetricsConfig.class);
private static final Joiner DOT = Joiner.on('.');

// Disable metrics by default for wide tables to prevent excessive metadata
private static final MetricsMode DEFAULT_MODE =
MetricsModes.fromString(DEFAULT_WRITE_METRICS_MODE_DEFAULT);
private static final MetricsConfig DEFAULT = new MetricsConfig(ImmutableMap.of(), DEFAULT_MODE);
private static final MetricsConfig DEFAULT =
new MetricsConfig(ImmutableMap.of(), DEFAULT_MODE, ImmutableMap.of());
private static final MetricsConfig POSITION_DELETE_MODE =
new MetricsConfig(
ImmutableMap.of(
MetadataColumns.DELETE_FILE_PATH.name(),
MetricsModes.Full.get(),
MetadataColumns.DELETE_FILE_POS.name(),
MetricsModes.Full.get()),
DEFAULT_MODE);
MetricsModes.None.get(),
ImmutableMap.of(
MetadataColumns.DELETE_FILE_PATH.fieldId(),
MetadataColumns.DELETE_FILE_PATH.name(),
MetadataColumns.DELETE_FILE_POS.fieldId(),
MetadataColumns.DELETE_FILE_POS.name()));

private final Map<String, MetricsMode> columnModes;
private final MetricsMode defaultMode;
private final Map<Integer, String> idToName;

private MetricsConfig(Map<String, MetricsMode> columnModes, MetricsMode defaultMode) {
private MetricsConfig(
Map<String, MetricsMode> columnModes,
MetricsMode defaultMode,
Map<Integer, String> idToName) {
this.columnModes = SerializableMap.copyOf(columnModes).immutableMap();
this.defaultMode = defaultMode;
this.idToName = idToName != null ? SerializableMap.copyOf(idToName).immutableMap() : null;
}

public static MetricsConfig getDefault() {
Expand All @@ -84,13 +94,23 @@ public static MetricsConfig forPositionDelete() {
* Creates a metrics config from table configuration.
*
* @param props table configuration
* @deprecated use {@link MetricsConfig#forTable(Table)}. Will be removed in 2.0.0
* @deprecated use {@link MetricsConfig#forTable(Table)}. Will be removed in 1.13.0
*/
@Deprecated
public static MetricsConfig fromProperties(Map<String, String> props) {
return from(props, null, null);
}

/**
* Validates metrics config properties with the given schema.
*
* @param props table properties
* @param schema table schema
*/
public static void validate(Map<String, String> props, Schema schema) {
from(props, schema, null).validateReferencedColumns(schema);
}

/**
* Creates a metrics config from a table.
*
Expand All @@ -100,32 +120,42 @@ public static MetricsConfig forTable(Table table) {
return from(table.properties(), table.schema(), table.sortOrder());
}

/**
* Creates a metrics config for a position delete file.
*
* @param table an Iceberg table
* @deprecated This method is deprecated as of version 1.11.0 and will be removed in 1.12.0.
* Position deletes that include row data are no longer supported. Use {@link
* #forPositionDelete()} instead.
*/
@Deprecated
public static MetricsConfig forPositionDelete(Table table) {
ImmutableMap.Builder<String, MetricsMode> columnModes = ImmutableMap.builder();
public Iterable<Integer> metricsFieldIds() {
Preconditions.checkState(idToName != null, "Cannot resolve column mode by ID: missing schema");
return idToName.keySet();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return an empty list if idToName is null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think it should fail if idToName is null because there could be a configuration, but the config wasn't initialized the right way. If idToName is null, then it was initialized without a schema, which we want to stop doing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this (and columnMode(int)) to check that idToName is present and throw an exception if not. That way, when we get rid of fromProperties, we can get rid of the check.

}

columnModes.put(MetadataColumns.DELETE_FILE_PATH.name(), MetricsModes.Full.get());
columnModes.put(MetadataColumns.DELETE_FILE_POS.name(), MetricsModes.Full.get());
public MetricsMode columnMode(int id) {
Preconditions.checkState(idToName != null, "Cannot resolve column mode by ID: missing schema");
String name = idToName.get(id);
if (name != null) {
return columnMode(name);
}

MetricsConfig tableConfig = forTable(table);
return defaultMode;
}

MetricsMode defaultMode = tableConfig.defaultMode;
tableConfig.columnModes.forEach(
(columnAlias, mode) -> {
String positionDeleteColumnAlias =
DOT.join(MetadataColumns.DELETE_FILE_ROW_FIELD_NAME, columnAlias);
columnModes.put(positionDeleteColumnAlias, mode);
});
public MetricsMode columnMode(String columnAlias) {
return columnModes.getOrDefault(columnAlias, defaultMode);
}

return new MetricsConfig(columnModes.build(), defaultMode);
public void validateReferencedColumns(Schema schema) {
for (String column : columnModes.keySet()) {
Types.NestedField field = schema.findField(column);
ValidationException.check(
field != null,
"Invalid metrics config, could not find column %s from table prop %s in schema %s",
column,
METRICS_MODE_COLUMN_CONF_PREFIX + column,
schema);

ValidationException.check(
Comment thread
rdblue marked this conversation as resolved.
null == idToName || column.equals(idToName.get(field.fieldId())),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was thinking about the field rename and preserve the field id but I believe it's already covered in schema update https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/SchemaUpdate.java#L517-L524

"Incorrect field name for id %s: %s (expected %s)",
field.fieldId(),
column,
idToName.get(field.fieldId()));
}
}

static Set<Integer> limitFieldIds(Schema schema, int limit) {
Expand Down Expand Up @@ -225,6 +255,7 @@ public Set<Integer> map(
*/
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();

// Handle user override of default mode
Expand All @@ -237,13 +268,20 @@ public static MetricsConfig from(Map<String, String> props, Schema schema, SortO
} else if (schema == null) {
defaultMode = DEFAULT_MODE;
} else {
if (TypeUtil.getProjectedIds(schema).size() <= maxInferredDefaultColumns) {
Set<Integer> ids = TypeUtil.getProjectedIds(schema);
Comment thread
rdblue marked this conversation as resolved.
if (ids.size() <= maxInferredDefaultColumns) {
for (int id : ids) {
idToName.put(id, schema.findColumnName(id));
}

// 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)) {
columnModes.put(schema.findColumnName(id), DEFAULT_MODE);
String name = schema.findColumnName(id);
idToName.put(id, name);
columnModes.put(name, DEFAULT_MODE);
}

// all other columns don't use metrics
Expand All @@ -254,18 +292,29 @@ public static MetricsConfig from(Map<String, String> props, Schema schema, SortO
// 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(sc -> columnModes.put(sc, sortedColDefaultMode));
sortedCols.forEach(
name -> {
columnModes.put(name, sortedColDefaultMode);
Types.NestedField field = schema != null ? schema.findField(name) : null;
if (field != null) {
idToName.put(field.fieldId(), name);
}
});

// Handle user overrides of defaults
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;
if (field != null) {
idToName.put(field.fieldId(), columnAlias);
}
}
}

return new MetricsConfig(columnModes, defaultMode);
return new MetricsConfig(columnModes, defaultMode, idToName);
}

/**
Expand Down Expand Up @@ -309,19 +358,4 @@ private static MetricsMode parseMode(String modeString, MetricsMode fallback, St
return fallback;
}
}

public void validateReferencedColumns(Schema schema) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were moved above the private and static methods. It was hard to find them before.

for (String column : columnModes.keySet()) {
ValidationException.check(
schema.findField(column) != null,
"Invalid metrics config, could not find column %s from table prop %s in schema %s",
column,
METRICS_MODE_COLUMN_CONF_PREFIX + column,
schema);
}
}

public MetricsMode columnMode(String columnAlias) {
return columnModes.getOrDefault(columnAlias, defaultMode);
}
}
27 changes: 26 additions & 1 deletion core/src/main/java/org/apache/iceberg/MetricsModes.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ public static MetricsMode fromString(String mode) {
*
* <p>Implementations must be immutable.
*/
public interface MetricsMode extends Serializable {}
public interface MetricsMode extends Serializable {
default boolean hasBounds() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used anywhere? it's not clear from the PR description why this is being added

throw new UnsupportedOperationException(
"Unexpected implementation of MetricsMode without hasBounds");
}
}

/**
* Under this mode, value_counts, null_value_counts, nan_value_counts, lower_bounds, upper_bounds
Expand All @@ -72,6 +77,11 @@ public static None get() {
return INSTANCE;
}

@Override
public boolean hasBounds() {
return false;
}

@Override
public String toString() {
return "none";
Expand All @@ -86,6 +96,11 @@ public static Counts get() {
return INSTANCE;
}

@Override
public boolean hasBounds() {
return false;
}

@Override
public String toString() {
return "counts";
Expand All @@ -108,6 +123,11 @@ public static Truncate withLength(int length) {
return new Truncate(length);
}

@Override
public boolean hasBounds() {
return true;
}

public int length() {
return length;
}
Expand Down Expand Up @@ -145,6 +165,11 @@ public static Full get() {
return INSTANCE;
}

@Override
public boolean hasBounds() {
return true;
}

@Override
public String toString() {
return "full";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public Map<String, String> apply() {

// Validate the metrics
if (base != null && base.schema() != null) {
MetricsConfig.fromProperties(newProperties).validateReferencedColumns(base.schema());
MetricsConfig.validate(newProperties, base.schema());
}

return newProperties;
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/org/apache/iceberg/TableMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ static TableMetadata newTableMetadata(

// Validate the metrics configuration. Note: we only do this on new tables to we don't
// break existing tables.
MetricsConfig.fromProperties(properties).validateReferencedColumns(schema);
MetricsConfig.validate(properties, schema);

PropertyUtil.validateCommitProperties(properties);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ public void testBoundsWithSpecificValues() {
@SuppressWarnings("deprecation")
void testBoundsForAllMetricsModes(String metricsMode) {
MetricsConfig metricsConfig =
MetricsConfig.fromProperties(
ImmutableMap.of(TableProperties.DEFAULT_WRITE_METRICS_MODE, metricsMode));
MetricsConfig.from(
ImmutableMap.of(TableProperties.DEFAULT_WRITE_METRICS_MODE, metricsMode), SCHEMA, null);
Metrics metrics =
FileGenerationUtil.generateRandomMetrics(
SCHEMA,
Expand Down
24 changes: 15 additions & 9 deletions core/src/test/java/org/apache/iceberg/TestMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ public void testMetricsModeForNestedStructFields() throws IOException {
MetricsModes.None.get().toString(),
TableProperties.METRICS_MODE_COLUMN_CONF_PREFIX + "nestedStructCol.longCol",
MetricsModes.Full.get().toString());
MetricsConfig config = MetricsConfig.fromProperties(properties);
MetricsConfig config = MetricsConfig.from(properties, NESTED_SCHEMA, null);

Metrics metrics = getMetrics(NESTED_SCHEMA, config, buildNestedTestRecord());
MetricsWithStats metricsWithStats =
Expand Down Expand Up @@ -590,7 +590,8 @@ public void testNoneMetricsMode() throws IOException {
Metrics metrics =
getMetrics(
NESTED_SCHEMA,
MetricsConfig.fromProperties(ImmutableMap.of("write.metadata.metrics.default", "none")),
MetricsConfig.from(
ImmutableMap.of("write.metadata.metrics.default", "none"), NESTED_SCHEMA, null),
buildNestedTestRecord());
MetricsWithStats metricsWithStats =
new MetricsWithStats(metrics, MetricsUtil.fromMetrics(NESTED_SCHEMA, metrics));
Expand All @@ -613,8 +614,8 @@ public void testCountsMetricsMode() throws IOException {
Metrics metrics =
getMetrics(
NESTED_SCHEMA,
MetricsConfig.fromProperties(
ImmutableMap.of("write.metadata.metrics.default", "counts")),
MetricsConfig.from(
ImmutableMap.of("write.metadata.metrics.default", "counts"), NESTED_SCHEMA, null),
buildNestedTestRecord());
MetricsWithStats metricsWithStats =
new MetricsWithStats(metrics, MetricsUtil.fromMetrics(NESTED_SCHEMA, metrics));
Expand All @@ -638,7 +639,8 @@ public void testFullMetricsMode() throws IOException {
Metrics metrics =
getMetrics(
NESTED_SCHEMA,
MetricsConfig.fromProperties(ImmutableMap.of("write.metadata.metrics.default", "full")),
MetricsConfig.from(
ImmutableMap.of("write.metadata.metrics.default", "full"), NESTED_SCHEMA, null),
buildNestedTestRecord());
MetricsWithStats metricsWithStats =
new MetricsWithStats(metrics, MetricsUtil.fromMetrics(NESTED_SCHEMA, metrics));
Expand Down Expand Up @@ -675,8 +677,10 @@ public void testTruncateStringMetricsMode() throws IOException {
Metrics metrics =
getMetrics(
singleStringColSchema,
MetricsConfig.fromProperties(
ImmutableMap.of("write.metadata.metrics.default", "truncate(10)")),
MetricsConfig.from(
ImmutableMap.of("write.metadata.metrics.default", "truncate(10)"),
singleStringColSchema,
null),
record);
MetricsWithStats metricsWithStats =
new MetricsWithStats(metrics, MetricsUtil.fromMetrics(singleStringColSchema, metrics));
Expand All @@ -702,8 +706,10 @@ public void testTruncateBinaryMetricsMode() throws IOException {
Metrics metrics =
getMetrics(
singleBinaryColSchema,
MetricsConfig.fromProperties(
ImmutableMap.of("write.metadata.metrics.default", "truncate(5)")),
MetricsConfig.from(
ImmutableMap.of("write.metadata.metrics.default", "truncate(5)"),
singleBinaryColSchema,
null),
record);
MetricsWithStats metricsWithStats =
new MetricsWithStats(metrics, MetricsUtil.fromMetrics(singleBinaryColSchema, metrics));
Expand Down
Loading