This is an automated email from the ASF dual-hosted git repository.
raghavyadav01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 068b458883d Add optional metadata map to FieldSpec (#18984)
068b458883d is described below
commit 068b458883d645ee2c686ab2a5e123ef2ffe8cff
Author: Navina Ramesh <[email protected]>
AuthorDate: Tue Jul 14 17:29:37 2026 -0400
Add optional metadata map to FieldSpec (#18984)
Add an optional, additive Map<String, String> metadata to FieldSpec for
free-form per-column metadata: @JsonInclude(NON_EMPTY) so it is omitted when
unset/empty, excluded from isBackwardCompatibleWith, and round-tripped via
the
shared appendFieldIdAndAliases helper (so TimeFieldSpec preserves it too).
The keys and their interpretation are defined by whoever populates it; the
core
schema attaches no semantics. Rolling-upgrade safe: old readers ignore the
unknown property (concrete subclasses are
@JsonIgnoreProperties(ignoreUnknown)),
new writers omit it when empty.
---
.../java/org/apache/pinot/spi/data/FieldSpec.java | 36 ++++++--
.../org/apache/pinot/spi/data/FieldSpecTest.java | 96 ++++++++++++++++++++++
2 files changed, 127 insertions(+), 5 deletions(-)
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
index c5c3a491f6c..5ecefcf9f85 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
@@ -38,6 +38,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
import org.apache.pinot.spi.utils.BooleanUtils;
import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.BytesUtils;
@@ -172,6 +174,14 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
@Nullable
protected List<String> _aliases;
+ // Optional, free-form per-column metadata. It is additive, excluded from
backward-compatibility
+ // checks, and omitted from serialization when unset/empty. The keys and
their interpretation are
+ // defined by whoever populates it; the core schema attaches no semantics to
it.
+ @JsonProperty("metadata")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @Nullable
+ protected Map<String, String> _metadata;
+
protected String _name;
protected DataType _dataType;
protected boolean _singleValueField = true;
@@ -249,7 +259,7 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
public void setTags(@Nullable List<String> tags) {
- _tags = tags;
+ _tags = CollectionUtils.isEmpty(tags) ? null : tags;
}
@Nullable
@@ -267,7 +277,16 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
public void setAliases(@Nullable List<String> aliases) {
- _aliases = aliases == null || aliases.isEmpty() ? null : aliases;
+ _aliases = CollectionUtils.isEmpty(aliases) ? null : aliases;
+ }
+
+ @Nullable
+ public Map<String, String> getMetadata() {
+ return _metadata;
+ }
+
+ public void setMetadata(@Nullable Map<String, String> metadata) {
+ _metadata = MapUtils.isEmpty(metadata) ? null : metadata;
}
public DataType getDataType() {
@@ -624,7 +643,7 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
return jsonObject;
}
- /// Appends `fieldId` and `aliases` (when set) to the given JSON object.
+ /// Appends `fieldId`, `aliases`, and `metadata` (when set) to the given
JSON object.
///
/// Subclasses that build JSON without calling [FieldSpec#toJsonObject()],
such as [TimeFieldSpec], use this helper
/// to preserve these fields during schema round-trip serialization.
@@ -639,6 +658,12 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
jsonObject.set("aliases", aliasesArray);
}
+ if (MapUtils.isNotEmpty(_metadata)) {
+ ObjectNode metadataNode = jsonObject.putObject("metadata");
+ for (Map.Entry<String, String> entry : _metadata.entrySet()) {
+ metadataNode.put(entry.getKey(), entry.getValue());
+ }
+ }
}
protected void appendDefaultNullValue(ObjectNode jsonNode) {
@@ -716,14 +741,15 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
&& Objects.equals(_description, that._description)
&& Objects.equals(_tags, that._tags)
&& Objects.equals(_fieldId, that._fieldId)
- && Objects.equals(_aliases, that._aliases);
+ && Objects.equals(_aliases, that._aliases)
+ && Objects.equals(_metadata, that._metadata);
}
@Override
public int hashCode() {
return Objects.hash(_name, _dataType, _singleValueField, _notNull,
_maxLength, _maxLengthExceedStrategy,
_allowTrailingZeros, _dataType.hashCode(_defaultNullValue),
_transformFunction, _virtualColumnProvider,
- _description, _tags, _fieldId, _aliases);
+ _description, _tags, _fieldId, _aliases, _metadata);
}
/**
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
index b156bb84865..fe5aa979181 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.pinot.spi.utils.ByteArray;
@@ -776,4 +777,99 @@ public class FieldSpecTest {
assertThat(newSpec.isBackwardCompatibleWith(oldSpec)).isTrue();
}
+
+ @Test
+ public void testMetadataSerdeRoundtrip()
+ throws Exception {
+ DimensionFieldSpec fieldSpec = new DimensionFieldSpec("addr", JSON, true);
+ fieldSpec.setMetadata(Map.of("2", "person"));
+
+ String json = fieldSpec.toJsonObject().toString();
+ assertThat(json).contains("\"metadata\":{\"2\":\"person\"}");
+
+ DimensionFieldSpec deserialized = JsonUtils.stringToObject(json,
DimensionFieldSpec.class);
+ assertThat(deserialized.getMetadata()).isEqualTo(Map.of("2", "person"));
+
+ String jacksonJson = JsonUtils.objectToString(fieldSpec);
+ DimensionFieldSpec fromJackson = JsonUtils.stringToObject(jacksonJson,
DimensionFieldSpec.class);
+ assertThat(fromJackson.getMetadata()).isEqualTo(Map.of("2", "person"));
+ }
+
+ @Test
+ public void testMetadataOmittedWhenNotSet()
+ throws Exception {
+ DimensionFieldSpec fieldSpec = new DimensionFieldSpec("col1", INT, true);
+
+ String json = fieldSpec.toJsonObject().toString();
+ assertThat(json).as("metadata should be absent when
null").doesNotContain("metadata");
+
+ String jacksonJson = JsonUtils.objectToString(fieldSpec);
+ assertThat(jacksonJson).as("metadata should be absent when
null").doesNotContain("metadata");
+ }
+
+ @Test
+ public void testEmptyMetadataOmittedFromJson()
+ throws Exception {
+ DimensionFieldSpec fieldSpec = new DimensionFieldSpec("col1", INT, true);
+ fieldSpec.setMetadata(Map.of());
+ assertThat(fieldSpec.getMetadata()).as("empty metadata normalizes to
null").isNull();
+
+ String json = fieldSpec.toJsonObject().toString();
+ assertThat(json).as("empty metadata should be
absent").doesNotContain("metadata");
+ assertThat(JsonUtils.stringToObject(json,
DimensionFieldSpec.class)).isEqualTo(fieldSpec);
+
+ String jacksonJson = JsonUtils.objectToString(fieldSpec);
+ assertThat(jacksonJson).as("empty metadata absent
(Jackson)").doesNotContain("metadata");
+ assertThat(JsonUtils.stringToObject(jacksonJson,
DimensionFieldSpec.class)).isEqualTo(fieldSpec);
+ }
+
+ @Test
+ public void testTimeFieldSpecRoundTripsMetadataThroughSchema()
+ throws Exception {
+ // TimeFieldSpec.toJsonObject() builds its JSON via the shared
appendFieldIdAndAliases helper;
+ // ensure metadata round-trips through schema (de)serialization for legacy
TIME columns too.
+ TimeFieldSpec timeFieldSpec = new TimeFieldSpec(new
TimeGranularitySpec(LONG, TimeUnit.DAYS, "ts"));
+ timeFieldSpec.setMetadata(Map.of("7", "event_ts"));
+
+ Schema schema = new Schema();
+ schema.setSchemaName("ts_schema");
+ schema.addField(timeFieldSpec);
+
+ String json = schema.toSingleLineJsonString();
+ assertThat(json).contains("\"metadata\":{\"7\":\"event_ts\"}");
+
+ FieldSpec deserialized = Schema.fromString(json).getFieldSpecFor("ts");
+ assertThat(deserialized.getMetadata()).isEqualTo(Map.of("7", "event_ts"));
+ }
+
+ @Test
+ public void testOldJsonWithoutMetadataDeserializesCleanly()
+ throws Exception {
+ String oldJson = "{\"name\":\"col1\",\"dataType\":\"STRING\"}";
+ DimensionFieldSpec fieldSpec = JsonUtils.stringToObject(oldJson,
DimensionFieldSpec.class);
+ assertThat(fieldSpec.getMetadata()).isNull();
+ }
+
+ @Test
+ public void testMetadataInEqualsAndHashCode() {
+ DimensionFieldSpec spec1 = new DimensionFieldSpec("col1", JSON, true);
+ DimensionFieldSpec spec2 = new DimensionFieldSpec("col1", JSON, true);
+ spec2.setMetadata(Map.of("2", "person"));
+
+ assertThat(spec1).isNotEqualTo(spec2);
+ assertThat(spec1.hashCode()).isNotEqualTo(spec2.hashCode());
+
+ spec1.setMetadata(Map.of("2", "person"));
+ assertThat(spec1).isEqualTo(spec2);
+ assertThat(spec1.hashCode()).isEqualTo(spec2.hashCode());
+ }
+
+ @Test
+ public void testMetadataNotInBackwardCompatibility() {
+ DimensionFieldSpec oldSpec = new DimensionFieldSpec("col1", JSON, true);
+ DimensionFieldSpec newSpec = new DimensionFieldSpec("col1", JSON, true);
+ newSpec.setMetadata(Map.of("2", "person"));
+
+ assertThat(newSpec.isBackwardCompatibleWith(oldSpec)).isTrue();
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]