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

Jackie-Jiang 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 f46b0a9fa08 Resolve absent MAP/OPEN_STRUCT keys to a shared 
default-column-style NullDataSource (#19439)
f46b0a9fa08 is described below

commit f46b0a9fa0842cd4c5d6a3abffe5b39830c4a52c
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Sep 4 15:56:04 2026 -0700

    Resolve absent MAP/OPEN_STRUCT keys to a shared default-column-style 
NullDataSource (#19439)
---
 .../core/operator/blocks/ProjectionBlock.java      |  11 +-
 .../core/operator/filter/MapFilterOperator.java    | 106 ++-------
 .../transform/function/ItemTransformFunction.java  |  38 +--
 .../pinot/core/plan/AggregationPlanNode.java       |   5 +-
 .../filter/MapFilterOperatorOpenStructTest.java    |  80 ++++---
 .../ItemTransformFunctionNullBitmapTest.java       |  74 +++---
 .../impl/openstruct/OpenStructColumnSplitter.java  |   4 +
 .../segment/index/datasource/NullDataSource.java   |  57 +++++
 .../local/segment/index/map/BaseMapDataSource.java |  38 ++-
 .../local/segment/index/map/NullDataSource.java    | 255 ---------------------
 .../openstruct/ImmutableOpenStructDataSource.java  |  32 +--
 .../openstruct/MutableOpenStructDataSource.java    |  16 +-
 .../index/openstruct/MutableOpenStructIndex.java   |  10 +-
 .../index/openstruct/OpenStructNullDataSource.java | 195 ----------------
 .../index/openstruct/SparseKeyDataSource.java      |   7 +-
 .../BaseConstantValueVirtualColumnProvider.java    |   2 +
 .../DefaultNullValueVirtualColumnProviderTest.java | 148 +++++++++---
 .../index/datasource/NullDataSourceTest.java       | 135 +++++++++++
 .../segment/index/map/BaseMapDataSourceTest.java   |  71 ++++++
 .../ImmutableOpenStructDataSourceTest.java         |  34 ++-
 .../MutableOpenStructDataSourceTest.java           |  32 ++-
 .../openstruct/OpenStructNullDataSourceTest.java   | 133 -----------
 .../segment/spi/datasource/MapDataSource.java      |   3 +-
 .../spi/datasource/OpenStructDataSource.java       |  52 ++---
 24 files changed, 614 insertions(+), 924 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/ProjectionBlock.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/ProjectionBlock.java
index 0a9bcd66c30..c70929348c4 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/ProjectionBlock.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/ProjectionBlock.java
@@ -23,8 +23,6 @@ import 
org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.core.common.BlockValSet;
 import org.apache.pinot.core.common.DataBlockCache;
 import org.apache.pinot.core.operator.docvalsets.ProjectionBlockValSet;
-import org.apache.pinot.segment.local.segment.index.map.NullDataSource;
-import 
org.apache.pinot.segment.local.segment.index.openstruct.OpenStructNullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.MapDataSource;
 import org.apache.pinot.segment.spi.datasource.OpenStructDataSource;
@@ -88,15 +86,8 @@ public class ProjectionBlock implements ValueBlock {
     DataSource keyDataSource;
     if (columnDataSource instanceof MapDataSource) {
       keyDataSource = ((MapDataSource) 
columnDataSource).getDataSource(paths[1]);
-      if (keyDataSource == null) {
-        keyDataSource = new NullDataSource(paths[1]);
-      }
     } else if (columnDataSource instanceof OpenStructDataSource) {
-      OpenStructDataSource osDs = (OpenStructDataSource) columnDataSource;
-      keyDataSource = osDs.getDataSource(paths[1]);
-      if (keyDataSource == null) {
-        keyDataSource = OpenStructNullDataSource.forAbsentKey(osDs, paths[1]);
-      }
+      keyDataSource = ((OpenStructDataSource) 
columnDataSource).getDataSource(paths[1]);
     } else {
       throw new IllegalStateException("Path-based access requires MAP or 
OPEN_STRUCT column: " + paths[0]);
     }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/filter/MapFilterOperator.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/filter/MapFilterOperator.java
index 29889e6c069..9c62e3d2466 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/filter/MapFilterOperator.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/filter/MapFilterOperator.java
@@ -37,13 +37,11 @@ import 
org.apache.pinot.core.operator.ExplainAttributeBuilder;
 import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
 import 
org.apache.pinot.core.operator.filter.predicate.PredicateEvaluatorProvider;
 import org.apache.pinot.core.query.request.context.QueryContext;
-import 
org.apache.pinot.segment.local.segment.index.openstruct.OpenStructNullDataSource;
 import org.apache.pinot.segment.spi.IndexSegment;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.OpenStructDataSource;
 import org.apache.pinot.segment.spi.index.IndexService;
 import org.apache.pinot.segment.spi.index.IndexType;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
 import org.apache.pinot.segment.spi.index.reader.JsonIndexReader;
 import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
 import org.apache.pinot.spi.data.FieldSpec;
@@ -52,7 +50,8 @@ import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
 
 /// Filter operator for Map/OPEN_STRUCT matching. Dispatches in priority order:
 /// 1. Per-key materialized index (OPEN_STRUCT columns with per-key 
inverted/range/bloom indexes)
-/// 2. Sparse virtual scan / manifest short-circuit (unmaterialized keys with 
a sparse blob tier)
+/// 2. Sparse virtual scan (unmaterialized keys with a sparse blob tier); a 
key absent from the segment resolves to an
+///    all-null source and takes the per-key path
 /// 3. JSON index (JsonMatchFilterOperator)
 /// 4. Expression scan fallback (ExpressionFilterOperator)
 public class MapFilterOperator extends BaseFilterOperator {
@@ -107,92 +106,24 @@ public class MapFilterOperator extends BaseFilterOperator 
{
       return null;
     }
     OpenStructDataSource osDs = (OpenStructDataSource) columnDs;
-
-    if (osDs.isMaterialized(_keyName)) {
-      DataSource keyDs = osDs.getDataSource(_keyName);
-      return buildPerKeyFilterOperator(keyDs, queryContext, numDocs);
-    }
-
-    // Key not materialized but the segment is fully materialized — definitive 
absence.
-    if (osDs.isFullyMaterialized()) {
-      return buildAbsentKeyFilterOperator(osDs, queryContext, numDocs);
-    }
-
-    // Sparse tier: virtual source from blob, or null if manifest proves 
absence.
-    DataSource sparseKeyDs = osDs.getDataSource(_keyName);
-    if (sparseKeyDs == null) {
-      return buildAbsentKeyFilterOperator(osDs, queryContext, numDocs);
-    }
-    BaseFilterOperator jsonFastPath = trySparseJsonIndex(osDs, sparseKeyDs, 
queryContext, numDocs);
-    if (jsonFastPath != null) {
-      return jsonFastPath;
-    }
-    return buildPerKeyFilterOperator(sparseKeyDs, queryContext, numDocs);
-  }
-
-  /// Builds the filter for a key that is definitively absent from a fully 
materialized segment.
-  /// Every document answers the predicate identically, so it is folded to a 
match-all / match-none
-  /// once instead of scanning an all-null column.
-  ///
-  /// With null handling on, three-valued logic makes every value predicate 
UNKNOWN and nothing
-  /// matches. With it off the key reads as its type's default null value, so 
the predicate is
-  /// evaluated against that single value — notably NOT_EQ / NOT_IN then match 
every document.
-  ///
-  /// The value comes from {@link OpenStructNullDataSource#forAbsentKey}, the 
same source the
-  /// projection path feeds to `item()`, so filter and projection cannot 
disagree. For a key with no
-  /// declared child spec that factory falls back to STRING, which means a 
numeric range over an
-  /// undeclared key compares lexicographically — the same answer `item()` 
yields for it.
-  @Nullable
-  private BaseFilterOperator buildAbsentKeyFilterOperator(OpenStructDataSource 
osDs, QueryContext queryContext,
-      int numDocs) {
-    Predicate.Type type = _predicate.getType();
-    if (type == Predicate.Type.IS_NULL) {
-      return new MatchAllFilterOperator(numDocs);
-    }
-    if (type == Predicate.Type.IS_NOT_NULL) {
-      return EmptyFilterOperator.getInstance();
-    }
-    Predicate rewritten = rewritePredicateForKey(_predicate);
-    if (rewritten == null) {
-      return null;
-    }
-    if (queryContext.isNullHandlingEnabled()) {
-      return EmptyFilterOperator.getInstance();
-    }
-    DataSource keyDs = OpenStructNullDataSource.forAbsentKey(osDs, _keyName);
-    PredicateEvaluator evaluator = 
PredicateEvaluatorProvider.getPredicateEvaluator(rewritten, keyDs, 
queryContext);
-    Boolean matches = matchesDefaultNullValue(evaluator, 
keyDs.getForwardIndex());
-    if (matches == null) {
-      return null;
-    }
-    return matches ? new MatchAllFilterOperator(numDocs) : 
EmptyFilterOperator.getInstance();
-  }
-
-  /// Applies the evaluator to the default null value every document of an 
absent key reads as.
-  /// Returns `null` for a stored type the all-null forward index cannot 
serve, so the caller falls
-  /// through to the expression path rather than failing the query.
-  @Nullable
-  private static Boolean matchesDefaultNullValue(PredicateEvaluator evaluator, 
ForwardIndexReader<?> forwardIndex) {
-    switch (forwardIndex.getStoredType()) {
-      case INT:
-        return evaluator.applySV(forwardIndex.getInt(0, null));
-      case LONG:
-        return evaluator.applySV(forwardIndex.getLong(0, null));
-      case FLOAT:
-        return evaluator.applySV(forwardIndex.getFloat(0, null));
-      case DOUBLE:
-        return evaluator.applySV(forwardIndex.getDouble(0, null));
-      case BIG_DECIMAL:
-        return evaluator.applySV(forwardIndex.getBigDecimal(0, null));
-      case STRING:
-        return evaluator.applySV(forwardIndex.getString(0, null));
-      case BYTES:
-        return evaluator.applySV(forwardIndex.getBytes(0, null));
-      default:
-        return null;
+    DataSource keyDs = osDs.getDataSource(_keyName);
+    // Among unmaterialized keys only a sparse virtual source lacks a 
dictionary; an absent key folds through its
+    // single-entry dictionary, which is cheaper on the per-key path than 
consulting the blob's JSON index
+    if (!osDs.isMaterialized(_keyName) && keyDs.getDictionary() == null) {
+      BaseFilterOperator jsonFastPath = trySparseJsonIndex(osDs, keyDs, 
queryContext, numDocs);
+      if (jsonFastPath != null) {
+        return jsonFastPath;
+      }
     }
+    return buildPerKeyFilterOperator(keyDs, queryContext, numDocs);
   }
 
+  /// Builds the filter over the key's data source. A key absent from the 
segment resolves to an all-null source whose
+  /// single-entry dictionary folds every value predicate to always-true or 
always-false, so no document is scanned:
+  /// with null handling on every document is null, so IS_NULL matches 
everything and nothing matches a value
+  /// predicate; with it off the key reads as its default null value, so 
notably NOT_EQ / NOT_IN match every document.
+  /// An undeclared key is typed STRING, so a numeric range over it compares 
lexicographically, the same answer `item()`
+  /// yields for it.
   @Nullable
   private BaseFilterOperator buildPerKeyFilterOperator(DataSource keyDs, 
QueryContext queryContext, int numDocs) {
     switch (_predicate.getType()) {
@@ -258,7 +189,8 @@ public class MapFilterOperator extends BaseFilterOperator {
       default:
         return null;
     }
-    if (values.contains(FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_STRING)) {
+    // Docs without the key read as the key's default null value, which the 
JSON index cannot see
+    if (values.contains((String) 
sparseKeyDs.getDataSourceMetadata().getFieldSpec().getDefaultNullValue())) {
       return null;
     }
     if (negated && queryContext.isNullHandlingEnabled()) {
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunction.java
index f19ea36f8d4..86722c286f9 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunction.java
@@ -25,8 +25,6 @@ import javax.annotation.Nullable;
 import org.apache.pinot.core.operator.ColumnContext;
 import org.apache.pinot.core.operator.blocks.ValueBlock;
 import org.apache.pinot.core.operator.transform.TransformResultMetadata;
-import org.apache.pinot.segment.local.segment.index.map.NullDataSource;
-import 
org.apache.pinot.segment.local.segment.index.openstruct.OpenStructNullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
 import org.apache.pinot.segment.spi.datasource.MapDataSource;
@@ -64,21 +62,11 @@ public class ItemTransformFunction extends 
BaseTransformFunction {
     DataSource dataSource = columnContextMap.get(column).getDataSource();
     Preconditions.checkState(dataSource instanceof MapDataSource || dataSource 
instanceof OpenStructDataSource,
         "Column: %s must be a MAP or OPEN_STRUCT column", column);
-    DataSource valueDataSource;
-    if (dataSource instanceof MapDataSource) {
-      valueDataSource = ((MapDataSource) dataSource).getDataSource(key);
-      if (valueDataSource == null) {
-        valueDataSource = new NullDataSource(key);
-      }
-      _perKeyNullsAvailable = false;
-    } else {
-      OpenStructDataSource osDs = (OpenStructDataSource) dataSource;
-      valueDataSource = osDs.getDataSource(key);
-      if (valueDataSource == null) {
-        valueDataSource = OpenStructNullDataSource.forAbsentKey(osDs, key);
-      }
-      _perKeyNullsAvailable = true;
-    }
+    DataSource valueDataSource = dataSource instanceof MapDataSource
+        ? ((MapDataSource) dataSource).getDataSource(key) : 
((OpenStructDataSource) dataSource).getDataSource(key);
+    // Per-key nulls are exact whenever the key's source tracks them: every 
OPEN_STRUCT key does, a MAP key only when
+    // it is absent from the segment
+    _perKeyNullsAvailable = valueDataSource.getNullValueVector() != null;
     // Only expose the dictionary when the forward index is dict-encoded. A 
column can have a dictionary alongside
     // a RAW forward index (e.g. dict + inverted/range), in which case 
transformToDictIdsSV would fail because
     // BlockValueSet.getDictionaryIdsSV requires a dict-encoded forward index.
@@ -105,16 +93,12 @@ public class ItemTransformFunction extends 
BaseTransformFunction {
     return _dictionary;
   }
 
-  /// Null semantics differ between the two backing column types:
-  ///
-  /// - OPEN_STRUCT keeps a per-key presence bitmap (materialized into a
-  ///   {@link 
org.apache.pinot.segment.spi.index.reader.NullValueVectorReader} on both the 
mutable and sealed paths),
-  ///   so the per-key value set knows exactly which docs lack the key. Use it.
-  /// - MAP has no per-key null information — an absent key resolves to a
-  ///   {@link NullDataSource} that carries only a forward index, so the 
per-key value set would report "no nulls" for
-  ///   a key that is missing from every doc. Fall back to {@link 
BaseTransformFunction#getNullBitmap} which ORs the
-  ///   argument bitmaps, yielding the MAP column's own null bitmap: a 
conservative over-estimate that downstream
-  ///   null handling narrows further.
+  /// Uses the per-key null bitmap when the key's data source tracks nulls, 
which is exact: every OPEN_STRUCT key does
+  /// (the per-key presence bitmap is materialized into a null value vector on 
both the mutable and sealed paths), and
+  /// so does a key absent from a MAP column, whose all-null source marks 
every document null. A key present in a MAP
+  /// column carries no per-key null information, so fall back to 
[BaseTransformFunction#getNullBitmap] which ORs the
+  /// argument bitmaps, yielding the MAP column's own null bitmap: a 
conservative over-estimate that downstream null
+  /// handling narrows further.
   @Nullable
   @Override
   public RoaringBitmap getNullBitmap(ValueBlock valueBlock) {
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java 
b/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
index b3fc9244bc0..81aa6490ff5 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
@@ -304,15 +304,14 @@ public class AggregationPlanNode implements PlanNode {
     DataSource columnDs = segment.getDataSource(columnName, schema);
     if (columnDs instanceof OpenStructDataSource) {
       OpenStructDataSource osDs = (OpenStructDataSource) columnDs;
-      DataSource keyDs = osDs.isMaterialized(key) ? osDs.getDataSource(key) : 
null;
-      if (keyDs == null) {
+      if (!osDs.isMaterialized(key)) {
         return null;
       }
       // A consuming key column's dictionary can contain a phantom entry (the 
reserved default,
       // never observed in any doc); dictionary-based aggregation over it 
would diverge from the
       // sealed segment. The segment layer owns that invariant — force the 
scan path when the
       // dictionary is not exact.
-      return osDs.isKeyDictionaryExact(key) ? keyDs : null;
+      return osDs.isKeyDictionaryExact(key) ? osDs.getDataSource(key) : null;
     }
     return null;
   }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/filter/MapFilterOperatorOpenStructTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/filter/MapFilterOperatorOpenStructTest.java
index 247bbc0b451..cea361a37c1 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/filter/MapFilterOperatorOpenStructTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/filter/MapFilterOperatorOpenStructTest.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import javax.annotation.Nullable;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.common.request.context.FilterContext;
@@ -39,6 +40,7 @@ import org.apache.pinot.core.common.BlockDocIdIterator;
 import org.apache.pinot.core.common.BlockDocIdSet;
 import org.apache.pinot.core.operator.transform.function.ItemTransformFunction;
 import org.apache.pinot.core.query.request.context.QueryContext;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import 
org.apache.pinot.segment.local.segment.index.openstruct.FakeStringForwardIndex;
 import 
org.apache.pinot.segment.local.segment.index.openstruct.OpenStructSparseBlobReader;
 import 
org.apache.pinot.segment.local.segment.index.openstruct.SparseKeyDataSource;
@@ -54,13 +56,16 @@ import 
org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
 import org.apache.pinot.spi.data.ComplexFieldSpec;
 import org.apache.pinot.spi.data.DimensionFieldSpec;
 import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.roaringbitmap.buffer.MutableRoaringBitmap;
 import org.testng.annotations.Test;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.*;
-import static org.testng.Assert.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 
 
 public class MapFilterOperatorOpenStructTest {
@@ -70,7 +75,7 @@ public class MapFilterOperatorOpenStructTest {
 
   private static ExpressionContext itemExpr(String column, String key) {
     ExpressionContext colArg = ExpressionContext.forIdentifier(column);
-    ExpressionContext keyArg = 
ExpressionContext.forLiteral(FieldSpec.DataType.STRING, key);
+    ExpressionContext keyArg = ExpressionContext.forLiteral(DataType.STRING, 
key);
     FunctionContext fn = new FunctionContext(FunctionContext.Type.TRANSFORM,
         ItemTransformFunction.FUNCTION_NAME, Arrays.asList(colArg, keyArg));
     return ExpressionContext.forFunction(fn);
@@ -109,27 +114,35 @@ public class MapFilterOperatorOpenStructTest {
     when(qc.isNullHandlingEnabled()).thenReturn(nullHandlingEnabled);
     when(qc.isIndexUseAllowed(any(DataSource.class), any())).thenReturn(true);
     when(qc.isIndexUseAllowed(anyString(), any())).thenReturn(true);
+    // The dictionary-based IN evaluator caches the sorted values on the query 
context
+    //noinspection unchecked
+    when(qc.getOrComputeSharedValue(any(), any(), any())).thenAnswer(
+        inv -> ((Function<Object, Object>) 
inv.getArgument(2)).apply(inv.getArgument(1)));
     return qc;
   }
 
-  /// OPEN_STRUCT source that is fully materialized but does not hold {@code 
key}. Stubs the field
-  /// spec and doc count that {@code OpenStructNullDataSource.forAbsentKey} 
reads.
+  /// OPEN_STRUCT source that is fully materialized but does not hold `key`, 
so the key resolves to an all-null source.
   private static OpenStructDataSource mockFullyMaterializedAbsentKey(String 
key) {
     return mockFullyMaterializedAbsentKey(key, Map.of());
   }
 
-  /// OPEN_STRUCT source that is fully materialized but does not hold {@code 
key}. Stubs the field
-  /// spec and doc count that {@code OpenStructNullDataSource.forAbsentKey} 
reads. {@code children}
-  /// carries the declared child specs — pass an empty map for an undeclared 
key.
+  /// OPEN_STRUCT source that is fully materialized but does not hold `key`, 
so the key resolves to an all-null source
+  /// typed by the real `getValueFieldSpec` rule over `children`, the declared 
child specs; pass an empty map for an
+  /// undeclared key.
   private static OpenStructDataSource mockFullyMaterializedAbsentKey(String 
key, Map<String, FieldSpec> children) {
-    OpenStructDataSource osDs = mock(OpenStructDataSource.class);
+    OpenStructDataSource osDs = mockOpenStructSource(children);
     when(osDs.isMaterialized(key)).thenReturn(false);
     when(osDs.isFullyMaterialized()).thenReturn(true);
-    when(osDs.getFieldSpec()).thenReturn(
-        new ComplexFieldSpec(COLUMN, FieldSpec.DataType.OPEN_STRUCT, true, 
children));
-    DataSourceMetadata osMeta = mock(DataSourceMetadata.class);
-    when(osMeta.getNumDocs()).thenReturn(NUM_DOCS);
-    when(osDs.getDataSourceMetadata()).thenReturn(osMeta);
+    NullDataSource absentKeyDs = new 
NullDataSource(osDs.getValueFieldSpec(key), NUM_DOCS);
+    when(osDs.getDataSource(key)).thenReturn(absentKeyDs);
+    return osDs;
+  }
+
+  /// OPEN_STRUCT source mock whose key field specs come from the real 
`getValueFieldSpec` rule over `children`.
+  private static OpenStructDataSource mockOpenStructSource(Map<String, 
FieldSpec> children) {
+    OpenStructDataSource osDs = mock(OpenStructDataSource.class);
+    when(osDs.getFieldSpec()).thenReturn(new ComplexFieldSpec(COLUMN, 
DataType.OPEN_STRUCT, true, children));
+    when(osDs.getValueFieldSpec(anyString())).thenCallRealMethod();
     return osDs;
   }
 
@@ -153,22 +166,17 @@ public class MapFilterOperatorOpenStructTest {
       Map<String, FieldSpec> children, String[] blobs) {
     OpenStructSparseBlobReader blob = new OpenStructSparseBlobReader(
         new FakeStringForwardIndex(blobs), 
FakeStringForwardIndex.nullVector(blobs), NUM_DOCS);
-    OpenStructDataSource osDs = mock(OpenStructDataSource.class);
+    OpenStructDataSource osDs = mockOpenStructSource(children);
     when(osDs.isMaterialized(anyString())).thenReturn(false);
     when(osDs.isFullyMaterialized()).thenReturn(false);
-    when(osDs.getFieldSpec()).thenReturn(
-        new ComplexFieldSpec(COLUMN, FieldSpec.DataType.OPEN_STRUCT, true, 
children));
     DataSourceMetadata osMeta = mock(DataSourceMetadata.class);
     when(osMeta.getNumDocs()).thenReturn(NUM_DOCS);
     when(osDs.getDataSourceMetadata()).thenReturn(osMeta);
     when(osDs.getDataSource(anyString())).thenAnswer(inv -> {
       String key = inv.getArgument(0);
+      FieldSpec childSpec = osDs.getValueFieldSpec(key);
       if (manifest != null && !manifest.contains(key)) {
-        return null;
-      }
-      FieldSpec childSpec = children.get(key);
-      if (childSpec == null) {
-        childSpec = new DimensionFieldSpec(key, FieldSpec.DataType.STRING, 
true);
+        return new NullDataSource(childSpec, NUM_DOCS);
       }
       return new SparseKeyDataSource(childSpec, blob);
     });
@@ -194,7 +202,7 @@ public class MapFilterOperatorOpenStructTest {
     when(osDs.getDataSource(KEY)).thenReturn(keyDs);
 
     DataSourceMetadata meta = mock(DataSourceMetadata.class);
-    when(meta.getDataType()).thenReturn(FieldSpec.DataType.STRING);
+    when(meta.getDataType()).thenReturn(DataType.STRING);
     when(meta.isSorted()).thenReturn(false);
     when(meta.isSingleValue()).thenReturn(true);
     when(keyDs.getDataSourceMetadata()).thenReturn(meta);
@@ -285,7 +293,7 @@ public class MapFilterOperatorOpenStructTest {
     IndexSegment segment = mockSegment(osDs);
 
     Predicate predicate = new RangePredicate(itemExpr(COLUMN, "missing_key"), 
false, "100", false,
-        RangePredicate.UNBOUNDED, FieldSpec.DataType.LONG);
+        RangePredicate.UNBOUNDED, DataType.LONG);
     MapFilterOperator op = new MapFilterOperator(segment, predicate, 
mockQueryContext(), NUM_DOCS);
 
     assertEquals(countMatches(op), NUM_DOCS);
@@ -297,11 +305,11 @@ public class MapFilterOperatorOpenStructTest {
   @Test
   public void testAbsentDeclaredKeyRangeMatchesNothing() {
     OpenStructDataSource osDs = mockFullyMaterializedAbsentKey("missing_key",
-        Map.of("missing_key", new DimensionFieldSpec("missing_key", 
FieldSpec.DataType.LONG, true)));
+        Map.of("missing_key", new DimensionFieldSpec("missing_key", 
DataType.LONG, true)));
     IndexSegment segment = mockSegment(osDs);
 
     Predicate predicate = new RangePredicate(itemExpr(COLUMN, "missing_key"), 
false, "100", false,
-        RangePredicate.UNBOUNDED, FieldSpec.DataType.LONG);
+        RangePredicate.UNBOUNDED, DataType.LONG);
     MapFilterOperator op = new MapFilterOperator(segment, predicate, 
mockQueryContext(), NUM_DOCS);
 
     assertTrue(op.canOptimizeCount());
@@ -373,7 +381,7 @@ public class MapFilterOperatorOpenStructTest {
       assertFalse(op.toExplainString().contains("delegateTo:per_key_index"));
     } catch (Exception e) {
       // The per-key path still had to decline before the expression fallback 
was attempted.
-      verify(osDs).isFullyMaterialized();
+      verify(osDs).getDataSource("missing_key");
     }
   }
 
@@ -470,12 +478,12 @@ public class MapFilterOperatorOpenStructTest {
       blobs[i] = i % 2 == 0 ? "{\"latencyMs\":" + i + "}" : null;
     }
     Map<String, FieldSpec> children =
-        Map.of("latencyMs", new DimensionFieldSpec("latencyMs", 
FieldSpec.DataType.LONG, true));
+        Map.of("latencyMs", new DimensionFieldSpec("latencyMs", DataType.LONG, 
true));
     OpenStructDataSource osDs = mockSparseSegmentSource(List.of("latencyMs"), 
children, blobs);
     IndexSegment segment = mockSegment(osDs);
 
     Predicate range = new RangePredicate(itemExpr(COLUMN, "latencyMs"), false, 
"50", false,
-        RangePredicate.UNBOUNDED, FieldSpec.DataType.LONG);
+        RangePredicate.UNBOUNDED, DataType.LONG);
     MapFilterOperator op = new MapFilterOperator(segment, range, 
mockQueryContext(), NUM_DOCS);
     assertTrue(op.toExplainString().contains("delegateTo:per_key_index"));
     assertEquals(countMatches(op), 24);
@@ -495,7 +503,7 @@ public class MapFilterOperatorOpenStructTest {
       assertFalse(op.toExplainString().contains("delegateTo:per_key_index"));
     } catch (Exception e) {
       // Per-key path declined; expression fallback attempted but may fail on 
mock internals.
-      verify(osDs).isFullyMaterialized();
+      verify(osDs).getDataSource("region");
     }
   }
 
@@ -543,7 +551,7 @@ public class MapFilterOperatorOpenStructTest {
     when(osDs.getDataSource(KEY)).thenReturn(keyDs);
 
     DataSourceMetadata meta = mock(DataSourceMetadata.class);
-    when(meta.getDataType()).thenReturn(FieldSpec.DataType.STRING);
+    when(meta.getDataType()).thenReturn(DataType.STRING);
     when(meta.isSorted()).thenReturn(false);
     when(meta.isSingleValue()).thenReturn(true);
     when(keyDs.getDataSourceMetadata()).thenReturn(meta);
@@ -648,13 +656,23 @@ public class MapFilterOperatorOpenStructTest {
       longBlobs[i] = i % 2 == 0 ? "{\"latencyMs\":" + i + "}" : null;
     }
     Map<String, FieldSpec> children =
-        Map.of("latencyMs", new DimensionFieldSpec("latencyMs", 
FieldSpec.DataType.LONG, true));
+        Map.of("latencyMs", new DimensionFieldSpec("latencyMs", DataType.LONG, 
true));
     OpenStructDataSource c = withSparseJsonIndex(
         mockSparseSegmentSource(List.of("latencyMs"), children, longBlobs), 
jsonIndex);
     MapFilterOperator opC = new MapFilterOperator(mockSegment(c),
         makeEqPredicate(COLUMN, "latencyMs", "42"), mockQueryContext(), 
NUM_DOCS);
     assertTrue(opC.toExplainString().contains("delegateTo:per_key_index"));
 
+    // (d) EQ against the custom default null value declared on a key the 
manifest excludes: the key folds through its
+    //     dictionary, which holds that default, instead of consulting the 
JSON index
+    Map<String, FieldSpec> customDefault =
+        Map.of("missing", new DimensionFieldSpec("missing", DataType.STRING, 
true, "N/A"));
+    OpenStructDataSource d = 
withSparseJsonIndex(mockSparseSegmentSource(List.of("region"), customDefault), 
jsonIndex);
+    MapFilterOperator opD = new MapFilterOperator(mockSegment(d),
+        makeEqPredicate(COLUMN, "missing", "N/A"), mockQueryContext(), 
NUM_DOCS);
+    assertTrue(opD.toExplainString().contains("delegateTo:per_key_index"));
+    assertEquals(countMatches(opD), NUM_DOCS);
+
     verify(jsonIndex, never()).getMatchingDocIds(any(FilterContext.class));
   }
 }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunctionNullBitmapTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunctionNullBitmapTest.java
index a497b0b3960..a9d7331623b 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunctionNullBitmapTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ItemTransformFunctionNullBitmapTest.java
@@ -23,14 +23,15 @@ import java.util.Map;
 import org.apache.pinot.core.common.BlockValSet;
 import org.apache.pinot.core.operator.ColumnContext;
 import org.apache.pinot.core.operator.blocks.ProjectionBlock;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
 import org.apache.pinot.segment.spi.datasource.MapDataSource;
 import org.apache.pinot.segment.spi.datasource.OpenStructDataSource;
 import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
-import org.apache.pinot.spi.data.ComplexFieldSpec;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
 import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.roaringbitmap.RoaringBitmap;
@@ -39,25 +40,23 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.*;
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
 
 
-/// Covers {@link ItemTransformFunction#getNullBitmap} for both backing column 
types.
+/// Covers [ItemTransformFunction#getNullBitmap] for both backing column types.
 ///
-/// <p>OPEN_STRUCT keeps a per-key presence bitmap, so {@code getNullBitmap} 
reads the per-key value set and must return
-/// block-local indices (projected by
-/// {@link org.apache.pinot.core.operator.docvalsets.ProjectionBlockValSet}) 
rather than raw segment-level doc IDs.
+/// OPEN_STRUCT keeps a per-key presence bitmap, so `getNullBitmap` reads the 
per-key value set and must return
+/// block-local indices (projected by 
[org.apache.pinot.core.operator.docvalsets.ProjectionBlockValSet]) rather than 
raw
+/// segment-level doc IDs.
 ///
-/// <p>MAP has no per-key null information, so {@code getNullBitmap} must fall 
back to
-/// {@link BaseTransformFunction#getNullBitmap} — the OR of the argument 
bitmaps, which yields the MAP column's own
-/// null bitmap. Reading the per-key value set for a MAP column would report 
"no nulls" for a key that is absent from
-/// every doc, because an absent MAP key resolves to a
-/// {@link org.apache.pinot.segment.local.segment.index.map.NullDataSource} 
carrying only a forward index.
+/// MAP has no per-key null information for a key present in the column, so 
`getNullBitmap` must fall back to
+/// [BaseTransformFunction#getNullBitmap], the OR of the argument bitmaps, 
which yields the MAP column's own null
+/// bitmap. A key absent from the column resolves to an all-null
+/// [org.apache.pinot.segment.local.segment.index.datasource.NullDataSource], 
whose null value vector is exact, so the
+/// per-key value set is used for it.
 public class ItemTransformFunctionNullBitmapTest {
   private static final String COLUMN = "myMap";
   private static final String KEY = "foo";
@@ -78,9 +77,9 @@ public class ItemTransformFunctionNullBitmapTest {
   @Mock
   private DataSource _keyDataSource;
   @Mock
-  private DataSourceMetadata _keyMetadata;
+  private NullValueVectorReader _keyNullValueVector;
   @Mock
-  private DataSourceMetadata _parentMetadata;
+  private DataSourceMetadata _keyMetadata;
   @Mock
   private IdentifierTransformFunction _identifierArg;
   @Mock
@@ -94,7 +93,7 @@ public class ItemTransformFunctionNullBitmapTest {
     when(forwardIndex.isDictionaryEncoded()).thenReturn(false);
     doReturn(forwardIndex).when(_keyDataSource).getForwardIndex();
     when(_keyDataSource.getDataSourceMetadata()).thenReturn(_keyMetadata);
-    when(_keyMetadata.getDataType()).thenReturn(FieldSpec.DataType.STRING);
+    when(_keyMetadata.getDataType()).thenReturn(DataType.STRING);
     when(_keyMetadata.isSingleValue()).thenReturn(true);
 
     when(_identifierArg.getColumnName()).thenReturn(COLUMN);
@@ -118,6 +117,7 @@ public class ItemTransformFunctionNullBitmapTest {
   @Test
   public void testOpenStructReturnsBlockLocalPerKeyIndices() {
     when(_openStructDataSource.getDataSource(KEY)).thenReturn(_keyDataSource);
+    when(_keyDataSource.getNullValueVector()).thenReturn(_keyNullValueVector);
 
     RoaringBitmap projectedBitmap = RoaringBitmap.bitmapOf(0, 2);
     when(_perKeyBlockValSet.getNullBitmap()).thenReturn(projectedBitmap);
@@ -133,6 +133,7 @@ public class ItemTransformFunctionNullBitmapTest {
   @Test
   public void testOpenStructIgnoresParentColumnBitmap() {
     when(_openStructDataSource.getDataSource(KEY)).thenReturn(_keyDataSource);
+    when(_keyDataSource.getNullValueVector()).thenReturn(_keyNullValueVector);
     
when(_perKeyBlockValSet.getNullBitmap()).thenReturn(RoaringBitmap.bitmapOf(0, 
2));
 
     RoaringBitmap result = 
initFunction(_openStructDataSource).getNullBitmap(_projectionBlock);
@@ -144,22 +145,19 @@ public class ItemTransformFunctionNullBitmapTest {
   @Test
   public void testOpenStructReturnsNullWhenNoNulls() {
     when(_openStructDataSource.getDataSource(KEY)).thenReturn(_keyDataSource);
+    when(_keyDataSource.getNullValueVector()).thenReturn(_keyNullValueVector);
     when(_perKeyBlockValSet.getNullBitmap()).thenReturn(null);
 
     
assertNull(initFunction(_openStructDataSource).getNullBitmap(_projectionBlock));
   }
 
   /// A key absent from the OPEN_STRUCT segment resolves to an all-null
-  /// {@link 
org.apache.pinot.segment.local.segment.index.openstruct.OpenStructNullDataSource},
 which still exposes
-  /// per-key nulls — so the per-key value set stays authoritative.
+  /// 
[org.apache.pinot.segment.local.segment.index.datasource.NullDataSource], which 
still exposes per-key nulls, so
+  /// the per-key value set stays authoritative.
   @Test
   public void testOpenStructAbsentKeyStillUsesPerKeyBitmap() {
-    when(_openStructDataSource.getDataSource(KEY)).thenReturn(null);
-    when(_openStructDataSource.getFieldSpec()).thenReturn(
-        new ComplexFieldSpec(COLUMN, FieldSpec.DataType.OPEN_STRUCT, true,
-            Map.of(KEY, new DimensionFieldSpec(KEY, FieldSpec.DataType.STRING, 
true))));
-    
when(_openStructDataSource.getDataSourceMetadata()).thenReturn(_parentMetadata);
-    when(_parentMetadata.getNumDocs()).thenReturn(NUM_DOCS);
+    when(_openStructDataSource.getDataSource(KEY)).thenReturn(
+        new NullDataSource(new DimensionFieldSpec(KEY, DataType.STRING, true), 
NUM_DOCS));
 
     RoaringBitmap allNull = RoaringBitmap.bitmapOf(0, 1, 2, 3, 4);
     when(_perKeyBlockValSet.getNullBitmap()).thenReturn(allNull);
@@ -171,7 +169,7 @@ public class ItemTransformFunctionNullBitmapTest {
   }
 
   // 
---------------------------------------------------------------------------------------------
-  // MAP — no per-key null info, fall back to the MAP column's own 
(conservative) bitmap
+  // MAP — a present key has no per-key null info (fall back to the MAP 
column's bitmap), an absent key is all-null
   // 
---------------------------------------------------------------------------------------------
 
   /// The per-key value set reports "no nulls" (MAP keeps no per-key null 
vector), but the MAP column itself is null
@@ -190,18 +188,20 @@ public class ItemTransformFunctionNullBitmapTest {
   }
 
   /// A MAP key absent from the segment resolves to a
-  /// {@link org.apache.pinot.segment.local.segment.index.map.NullDataSource}, 
which carries no null value vector.
-  /// The fallback must still surface the MAP column's own nulls rather than 
reporting none.
+  /// 
[org.apache.pinot.segment.local.segment.index.datasource.NullDataSource], whose 
null value vector marks every
+  /// document null, so the per-key value set is authoritative and the MAP 
column's own bitmap must not widen it.
   @Test
-  public void testMapAbsentKeyFallsBackToParentColumnBitmap() {
-    when(_mapDataSource.getDataSource(KEY)).thenReturn(null);
-    when(_perKeyBlockValSet.getNullBitmap()).thenReturn(null);
-    
when(_identifierArg.getNullBitmap(_projectionBlock)).thenReturn(RoaringBitmap.bitmapOf(2));
+  public void testMapAbsentKeyUsesPerKeyBitmap() {
+    when(_mapDataSource.getDataSource(KEY)).thenReturn(
+        new NullDataSource(new DimensionFieldSpec(KEY, DataType.STRING, true), 
NUM_DOCS));
+
+    RoaringBitmap allNull = RoaringBitmap.bitmapOf(0, 1, 2, 3, 4);
+    when(_perKeyBlockValSet.getNullBitmap()).thenReturn(allNull);
 
     RoaringBitmap result = 
initFunction(_mapDataSource).getNullBitmap(_projectionBlock);
 
-    assertNotNull(result);
-    assertEquals(result, RoaringBitmap.bitmapOf(2));
+    assertEquals(result, allNull);
+    verify(_identifierArg, never()).getNullBitmap(any());
   }
 
   @Test
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java
index 01565e06605..cfa82d50f33 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java
@@ -376,6 +376,10 @@ public class OpenStructColumnSplitter implements 
ColumnarOpenStructIndexCreator
     RoaringBitmap presence = _presenceBitmaps.get(key);
     List<Object> values = _values.get(key);
 
+    // TODO: Honor the declared child field spec (field type, 
single/multi-value and custom default null value) instead
+    //   of synthesizing a single-value dimension of the stored type, so a 
document without the key reads the same as
+    //   through OpenStructDataSource.getValueFieldSpec, which returns the 
declared spec for a key absent from the
+    //   segment. See https://github.com/apache/pinot/issues/19466
     // Synthetic field spec for the materialized child. Its natural Pinot 
dimension null value is the value
     // stored for absent docs, so column metadata stays consistent with 
on-disk content.
     DimensionFieldSpec childFieldSpec = new 
DimensionFieldSpec(materializedCol, storedType, true);
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSource.java
new file mode 100644
index 00000000000..583fee40ee3
--- /dev/null
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSource.java
@@ -0,0 +1,57 @@
+/**
+ * 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.pinot.segment.local.segment.index.datasource;
+
+import 
org.apache.pinot.segment.local.segment.index.column.DefaultNullValueVirtualColumnProvider;
+import 
org.apache.pinot.segment.local.segment.index.readers.AllNullValueVectorReader;
+import 
org.apache.pinot.segment.local.segment.virtualcolumn.VirtualColumnContext;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.spi.data.FieldSpec;
+
+
+/// Data source for a column that holds no value in any document, such as a 
key that is absent from a complex column.
+///
+/// The column is built the same way as a default column, i.e. a schema column 
that is missing from the segment: a
+/// single-entry dictionary holding the field's default null value, a constant 
sorted forward index for a single-value
+/// field or constant multi-value forward and inverted indexes otherwise, and 
metadata reporting a cardinality of one
+/// with the default null value as both min and max. On top of that every 
document is marked null in the null value
+/// vector, so with null handling enabled the column reads as null rather than 
as the default value.
+///
+/// Immutable and safe to share across threads: every reader is stateless, and 
the null value vector materializes its
+/// bitmap lazily under a benign race.
+public class NullDataSource extends ImmutableDataSource {
+  private static final AllNullColumnProvider PROVIDER = new 
AllNullColumnProvider();
+
+  public NullDataSource(FieldSpec fieldSpec, int numDocs) {
+    this(new VirtualColumnContext(fieldSpec, numDocs));
+  }
+
+  private NullDataSource(VirtualColumnContext context) {
+    super(PROVIDER.buildMetadata(context), 
PROVIDER.buildColumnIndexContainer(context));
+  }
+
+  /// Default column provider that additionally marks every document null.
+  private static class AllNullColumnProvider extends 
DefaultNullValueVirtualColumnProvider {
+
+    @Override
+    public NullValueVectorReader buildNullValueVector(VirtualColumnContext 
context) {
+      return new AllNullValueVectorReader(context.getTotalDocCount());
+    }
+  }
+}
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSource.java
index 102154185e3..c1b51b807ce 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSource.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSource.java
@@ -25,7 +25,7 @@ import java.util.Set;
 import javax.annotation.Nullable;
 import org.apache.pinot.segment.local.segment.index.datasource.BaseDataSource;
 import 
org.apache.pinot.segment.local.segment.index.datasource.ImmutableDataSource;
-import org.apache.pinot.segment.spi.ColumnMetadata;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
 import org.apache.pinot.segment.spi.datasource.MapDataSource;
@@ -35,13 +35,9 @@ import 
org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
 import org.apache.pinot.segment.spi.index.reader.Dictionary;
 import org.apache.pinot.segment.spi.index.reader.MapIndexReader;
 import org.apache.pinot.spi.data.ComplexFieldSpec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 
 public abstract class BaseMapDataSource extends BaseDataSource implements 
MapDataSource {
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(BaseMapDataSource.class);
-
   protected final Map<String, DataSource> _keyDataSources;
 
   public BaseMapDataSource(DataSourceMetadata dataSourceMetadata, 
ColumnIndexContainer indexContainer) {
@@ -60,30 +56,22 @@ public abstract class BaseMapDataSource extends 
BaseDataSource implements MapDat
     return null;
   }
 
-  /// Get the Data Source representation of a single key within this map 
column.
-  ///
-  /// @param key to get the DataSource for
-  /// @return DataSource for the key
+  /// Returns the data source for a single key within this map column. A key 
absent from the map resolves to an
+  /// all-null [NullDataSource] typed as the map's value field.
   public DataSource getDataSource(String key) {
-    if (_keyDataSources.containsKey(key)) {
-      return _keyDataSources.get(key);
+    DataSource dataSource = _keyDataSources.get(key);
+    if (dataSource != null) {
+      return dataSource;
     }
-    Map<IndexType, IndexReader> indexes = getMapIndexReader().getIndexes(key);
-
+    MapIndexReader mapIndexReader = getMapIndexReader();
+    Map<IndexType, IndexReader> indexes = mapIndexReader.getIndexes(key);
     if (indexes == null) {
-      // The key does not exist in the map
-      return new NullDataSource(key);
+      return new NullDataSource(getFieldSpec().getValueFieldSpec(), 
getDataSourceMetadata().getNumDocs());
     }
-
-    try (ColumnIndexContainer indexContainer = new 
ColumnIndexContainer.FromMap(indexes)) {
-      ColumnMetadata keyMeta = getMapIndexReader().getColumnMetadata(key);
-      ImmutableDataSource dataSource = new ImmutableDataSource(keyMeta, 
indexContainer);
-      _keyDataSources.put(key, dataSource);
-      return dataSource;
-    } catch (Exception ex) {
-      LOGGER.error("Caught exception while creating key data source for key: 
{}", key, ex);
-    }
-    return null;
+    dataSource =
+        new ImmutableDataSource(mapIndexReader.getColumnMetadata(key), new 
ColumnIndexContainer.FromMap(indexes));
+    _keyDataSources.put(key, dataSource);
+    return dataSource;
   }
 
   public abstract MapIndexReader getMapIndexReader();
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/NullDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/NullDataSource.java
deleted file mode 100644
index caedc153591..00000000000
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/NullDataSource.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * 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.pinot.segment.local.segment.index.map;
-
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Nullable;
-import org.apache.pinot.segment.spi.datasource.DataSource;
-import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
-import org.apache.pinot.segment.spi.index.IndexReader;
-import org.apache.pinot.segment.spi.index.IndexType;
-import org.apache.pinot.segment.spi.index.StandardIndexes;
-import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
-import org.apache.pinot.segment.spi.index.reader.BloomFilterReader;
-import org.apache.pinot.segment.spi.index.reader.Dictionary;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReaderContext;
-import org.apache.pinot.segment.spi.index.reader.H3IndexReader;
-import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
-import org.apache.pinot.segment.spi.index.reader.JsonIndexReader;
-import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
-import org.apache.pinot.segment.spi.index.reader.RangeIndexReader;
-import org.apache.pinot.segment.spi.index.reader.TextIndexReader;
-import org.apache.pinot.segment.spi.index.reader.VectorIndexReader;
-import org.apache.pinot.segment.spi.partition.PartitionFunction;
-import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
-
-
-/// If a key does not exist in a Map Column, then the Map Data Source will 
return this NulLDataSource.
-/// The NullDataSource represents an INT column where every document has the 
Default Null Value.  Semantically,
-/// this means that if a key is not in a Map column, then the value will 
always resolve to "Null".
-public class NullDataSource implements DataSource {
-  private final NullDataSourceMetadata _md;
-  private final ColumnIndexContainer _indexes;
-
-  public NullDataSource(String name) {
-    _md = new NullDataSourceMetadata(name);
-    _indexes = new 
ColumnIndexContainer.FromMap(Map.of(StandardIndexes.forward(), new 
NullForwardIndex()));
-  }
-
-  @Override
-  public DataSourceMetadata getDataSourceMetadata() {
-    return _md;
-  }
-
-  @Override
-  public ColumnIndexContainer getIndexContainer() {
-    return _indexes;
-  }
-
-  @Override
-  public <R extends IndexReader> R getIndex(IndexType<?, R, ?> type) {
-    return type.getIndexReader(_indexes);
-  }
-
-  @Override
-  public ForwardIndexReader<?> getForwardIndex() {
-    return getIndex(StandardIndexes.forward());
-  }
-
-  @Nullable
-  @Override
-  public Dictionary getDictionary() {
-    return getIndex(StandardIndexes.dictionary());
-  }
-
-  @Nullable
-  @Override
-  public InvertedIndexReader<?> getInvertedIndex() {
-    return getIndex(StandardIndexes.inverted());
-  }
-
-  @Nullable
-  @Override
-  public RangeIndexReader<?> getRangeIndex() {
-    return getIndex(StandardIndexes.range());
-  }
-
-  @Nullable
-  @Override
-  public TextIndexReader getTextIndex() {
-    return getIndex(StandardIndexes.text());
-  }
-
-  @Nullable
-  @Override
-  public TextIndexReader getFSTIndex() {
-    return getIndex(StandardIndexes.fst());
-  }
-
-  @Nullable
-  @Override
-  public TextIndexReader getIFSTIndex() {
-    return getIndex(StandardIndexes.ifst());
-  }
-
-  @Nullable
-  @Override
-  public JsonIndexReader getJsonIndex() {
-    return getIndex(StandardIndexes.json());
-  }
-
-  @Nullable
-  @Override
-  public H3IndexReader getH3Index() {
-    return getIndex(StandardIndexes.h3());
-  }
-
-  @Nullable
-  @Override
-  public BloomFilterReader getBloomFilter() {
-    return getIndex(StandardIndexes.bloomFilter());
-  }
-
-  @Nullable
-  @Override
-  public NullValueVectorReader getNullValueVector() {
-    return getIndex(StandardIndexes.nullValueVector());
-  }
-
-  @Nullable
-  @Override
-  public VectorIndexReader getVectorIndex() {
-    return getIndex(StandardIndexes.vector());
-  }
-
-  public static class NullDataSourceMetadata implements DataSourceMetadata {
-    String _name;
-
-    NullDataSourceMetadata(String name) {
-      _name = name;
-    }
-
-    @Override
-    public FieldSpec getFieldSpec() {
-      return new DimensionFieldSpec(_name, FieldSpec.DataType.INT, true);
-    }
-
-    @Override
-    public boolean isSorted() {
-      return false;
-    }
-
-    @Override
-    public int getNumDocs() {
-      return 0;
-    }
-
-    @Override
-    public int getNumValues() {
-      return 0;
-    }
-
-    @Override
-    public int getMaxNumValuesPerMVEntry() {
-      return 0;
-    }
-
-    @Nullable
-    @Override
-    public Comparable getMinValue() {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT;
-    }
-
-    @Nullable
-    @Override
-    public Comparable getMaxValue() {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT;
-    }
-
-    @Nullable
-    @Override
-    public PartitionFunction getPartitionFunction() {
-      return null;
-    }
-
-    @Nullable
-    @Override
-    public Set<Integer> getPartitions() {
-      return null;
-    }
-
-    @Override
-    public int getCardinality() {
-      return 1;
-    }
-  }
-
-  public class NullForwardIndex implements 
ForwardIndexReader<ForwardIndexReaderContext> {
-    NullForwardIndex() {
-    }
-
-    @Override
-    public boolean isDictionaryEncoded() {
-      return false;
-    }
-
-    @Override
-    public boolean isSingleValue() {
-      return false;
-    }
-
-    @Override
-    public FieldSpec.DataType getStoredType() {
-      return FieldSpec.DataType.INT;
-    }
-
-    @Override
-    public int getInt(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT;
-    }
-
-    @Override
-    public long getLong(int docId, ForwardIndexReaderContext context) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public float getFloat(int docId, ForwardIndexReaderContext context) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public double getDouble(int docId, ForwardIndexReaderContext context) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getString(int docId, ForwardIndexReaderContext context) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void close() {
-    }
-  }
-}
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java
index fbc8c10dc64..6972604027b 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java
@@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.Nullable;
 import org.apache.pinot.segment.local.segment.index.datasource.BaseDataSource;
 import 
org.apache.pinot.segment.local.segment.index.datasource.ImmutableDataSource;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.Constants;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
@@ -34,13 +35,12 @@ import 
org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
 import org.apache.pinot.segment.spi.index.reader.JsonIndexReader;
 import org.apache.pinot.segment.spi.partition.PartitionFunction;
 import org.apache.pinot.spi.data.ComplexFieldSpec;
-import org.apache.pinot.spi.data.DimensionFieldSpec;
 import org.apache.pinot.spi.data.FieldSpec;
 
 
-/// Per-key {@link DataSource} accessor for sealed OPEN_STRUCT segments. Dense 
keys get
-/// materialized DataSources; sparse keys get virtual [SparseKeyDataSource]s 
backed by the
-/// shared blob parser. Manifest-absent keys return null (definitively absent).
+/// Per-key [DataSource] accessor for sealed OPEN_STRUCT segments. Dense keys 
get materialized DataSources; sparse keys
+/// get virtual [SparseKeyDataSource]s backed by the shared blob parser; keys 
absent from the segment (no sparse blob,
+/// or not listed in the sparse manifest) resolve to an all-null 
[NullDataSource].
 public class ImmutableOpenStructDataSource extends BaseDataSource implements 
OpenStructDataSource {
   private final ComplexFieldSpec _fieldSpec;
   private final Map<String, DataSource> _perKeyDataSources;
@@ -92,27 +92,17 @@ public class ImmutableOpenStructDataSource extends 
BaseDataSource implements Ope
   }
 
   @Override
-  @Nullable
   public DataSource getDataSource(String key) {
     DataSource ds = _perKeyDataSources.get(key);
     if (ds != null) {
       return ds;
     }
-    if (_sparseBlobReader == null) {
-      return null;
+    if (_sparseBlobReader == null || (_sparseKeys != null && 
!_sparseKeys.contains(key))) {
+      // Definitively absent: no sparse blob, or the sparse manifest does not 
list the key
+      return new NullDataSource(getValueFieldSpec(key), 
getDataSourceMetadata().getNumDocs());
     }
-    if (_sparseKeys != null && !_sparseKeys.contains(key)) {
-      return null;
-    }
-    return _sparseKeyDataSourceCache.computeIfAbsent(key, 
this::buildSparseKeyDataSource);
-  }
-
-  private DataSource buildSparseKeyDataSource(String key) {
-    FieldSpec childSpec = _fieldSpec.getChildFieldSpec(key);
-    if (childSpec == null) {
-      childSpec = new DimensionFieldSpec(key, FieldSpec.DataType.STRING, true);
-    }
-    return new SparseKeyDataSource(childSpec, _sparseBlobReader);
+    return _sparseKeyDataSourceCache.computeIfAbsent(key,
+        k -> new SparseKeyDataSource(getValueFieldSpec(k), _sparseBlobReader));
   }
 
   @Override
@@ -133,10 +123,8 @@ public class ImmutableOpenStructDataSource extends 
BaseDataSource implements Ope
   }
 
   @Override
-  @Nullable
   public DataSourceMetadata getDataSourceMetadata(String key) {
-    DataSource ds = getDataSource(key);
-    return ds != null ? ds.getDataSourceMetadata() : null;
+    return getDataSource(key).getDataSourceMetadata();
   }
 
   @Override
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSource.java
index 497e0967649..1f05cd8e8af 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSource.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSource.java
@@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.Nullable;
 import org.apache.pinot.segment.local.segment.index.datasource.BaseDataSource;
 import 
org.apache.pinot.segment.local.segment.index.datasource.ImmutableDataSource;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.ColumnMetadata;
 import org.apache.pinot.segment.spi.Constants;
 import org.apache.pinot.segment.spi.datasource.DataSource;
@@ -69,12 +70,12 @@ public class MutableOpenStructDataSource extends 
BaseDataSource implements OpenS
   }
 
   @Override
-  @Nullable
   public DataSource getDataSource(String key) {
-    // Live lookup, outside the memo: a key not yet observed may still be 
created by the ingestion thread.
+    // Live lookup, outside the memo: a key not yet observed may still be 
created by the ingestion thread, so an
+    // absent key resolves to a fresh all-null source instead of being memoised
     MutableKeyColumn col = _index.getKeyColumn(key);
     if (col == null) {
-      return null;
+      return new NullDataSource(getValueFieldSpec(key), _numDocs);
     }
     return _perKeyDataSourceCache.computeIfAbsent(key, k -> {
       Map<IndexType, IndexReader> indexes = new 
HashMap<>(_index.getIndexes(k));
@@ -118,19 +119,14 @@ public class MutableOpenStructDataSource extends 
BaseDataSource implements OpenS
   public Map<String, DataSource> getDataSources() {
     Map<String, DataSource> result = new HashMap<>();
     for (String key : _index.getKeys()) {
-      DataSource ds = getDataSource(key);
-      if (ds != null) {
-        result.put(key, ds);
-      }
+      result.put(key, getDataSource(key));
     }
     return result;
   }
 
   @Override
-  @Nullable
   public DataSourceMetadata getDataSourceMetadata(String key) {
-    DataSource ds = getDataSource(key);
-    return ds != null ? ds.getDataSourceMetadata() : null;
+    return getDataSource(key).getDataSourceMetadata();
   }
 
   @Override
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java
index 23bb33795a1..b1c81dac7b2 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java
@@ -196,11 +196,11 @@ public class MutableOpenStructIndex implements 
OpenStructIndexReader<ForwardInde
   /// publishes it via volatile copy-on-write.
   private MutableKeyColumn allocateKeyColumn(String key, DataType storedType) {
     String allocationContext = _openStructColumn + "$" + key;
-    // Use the standard dimension default for the resolved stored type, 
regardless of any child
-    // spec's own default: OpenStructColumnSplitter#writeDenseKeyColumn (the 
sealed build path)
-    // always derives the absent-doc default from a throwaway 
DimensionFieldSpec(key, storedType,
-    // true) rather than the real child spec, so mirroring that exactly is 
what keeps a doc's
-    // resolved value identical before and after seal.
+    // TODO: Use the declared child field spec's default null value, as 
OpenStructDataSource.getValueFieldSpec does for
+    //   a key absent from the segment. The standard dimension default of the 
stored type is kept for now because
+    //   OpenStructColumnSplitter#writeDenseKeyColumn (the sealed build path) 
derives the absent-doc default the same
+    //   way, and a doc's resolved value must stay identical before and after 
seal.
+    //   See https://github.com/apache/pinot/issues/19466
     Object defaultNullValue = 
FieldSpec.getDefaultNullValue(FieldSpec.FieldType.DIMENSION, storedType, null);
     MutableKeyColumn newCol =
         new MutableKeyColumn(key, storedType, defaultNullValue, 
_memoryManager, _capacity, allocationContext);
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
deleted file mode 100644
index 1b36dceff4d..00000000000
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * 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.pinot.segment.local.segment.index.openstruct;
-
-import java.math.BigDecimal;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Nullable;
-import org.apache.pinot.segment.local.segment.index.datasource.BaseDataSource;
-import 
org.apache.pinot.segment.local.segment.index.readers.AllNullValueVectorReader;
-import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
-import org.apache.pinot.segment.spi.datasource.OpenStructDataSource;
-import org.apache.pinot.segment.spi.index.StandardIndexes;
-import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReaderContext;
-import org.apache.pinot.segment.spi.partition.PartitionFunction;
-import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
-import org.apache.pinot.spi.data.FieldSpec.DataType;
-
-
-/// Typed all-null {@link org.apache.pinot.segment.spi.datasource.DataSource} 
for an OPEN_STRUCT key
-/// that is absent from a segment.
-///
-/// Unlike the MAP column's {@code NullDataSource} (which is hardcoded as INT 
with numDocs=0),
-/// this class uses the correct {@link DataType} from the OPEN_STRUCT child 
{@link FieldSpec} and
-/// reports the segment's actual doc count. Every document is null: the 
forward index returns the
-/// type-correct default null value, and the {@link NullValueVectorReader} 
returns a full-segment
-/// bitmap.
-///
-/// Extends {@link BaseDataSource}: every other index accessor (dictionary, 
inverted, range, text,
-/// bloom filter, vector, etc.) resolves to {@code null} automatically since 
the index container
-/// only holds a forward index and a null value vector.
-public class OpenStructNullDataSource extends BaseDataSource {
-  public OpenStructNullDataSource(FieldSpec fieldSpec, int numDocs) {
-    super(new AllNullMetadata(fieldSpec, numDocs), new 
ColumnIndexContainer.FromMap(Map.of(
-        StandardIndexes.forward(), new 
TypedNullForwardIndex(fieldSpec.getDataType().getStoredType()),
-        StandardIndexes.nullValueVector(), new 
AllNullValueVectorReader(numDocs))));
-  }
-
-  /// Creates an all-null DataSource for a key absent from this OPEN_STRUCT 
segment.
-  /// Resolves the key's type from the child FieldSpec, falling back to STRING.
-  public static OpenStructNullDataSource forAbsentKey(OpenStructDataSource 
osDs, String key) {
-    FieldSpec childSpec = osDs.getFieldSpec().getChildFieldSpec(key);
-    if (childSpec == null) {
-      childSpec = new DimensionFieldSpec(key, FieldSpec.DataType.STRING, true);
-    }
-    int numDocs = osDs.getDataSourceMetadata().getNumDocs();
-    return new OpenStructNullDataSource(childSpec, numDocs);
-  }
-
-  /// Forward index that returns the type-correct default null value for every 
document.
-  static class TypedNullForwardIndex implements 
ForwardIndexReader<ForwardIndexReaderContext> {
-    private final DataType _storedType;
-
-    TypedNullForwardIndex(DataType storedType) {
-      _storedType = storedType;
-    }
-
-    @Override
-    public boolean isDictionaryEncoded() {
-      return false;
-    }
-
-    @Override
-    public boolean isSingleValue() {
-      return true;
-    }
-
-    @Override
-    public DataType getStoredType() {
-      return _storedType;
-    }
-
-    @Override
-    public int getInt(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT;
-    }
-
-    @Override
-    public long getLong(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG;
-    }
-
-    @Override
-    public float getFloat(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_FLOAT;
-    }
-
-    @Override
-    public double getDouble(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_DOUBLE;
-    }
-
-    @Override
-    public BigDecimal getBigDecimal(int docId, ForwardIndexReaderContext 
context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BIG_DECIMAL;
-    }
-
-    @Override
-    public String getString(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_STRING;
-    }
-
-    @Override
-    public byte[] getBytes(int docId, ForwardIndexReaderContext context) {
-      return FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BYTES;
-    }
-
-    @Override
-    public void close() {
-    }
-  }
-
-  private static class AllNullMetadata implements DataSourceMetadata {
-    private final FieldSpec _fieldSpec;
-    private final int _numDocs;
-
-    AllNullMetadata(FieldSpec fieldSpec, int numDocs) {
-      _fieldSpec = fieldSpec;
-      _numDocs = numDocs;
-    }
-
-    @Override
-    public FieldSpec getFieldSpec() {
-      return _fieldSpec;
-    }
-
-    @Override
-    public boolean isSorted() {
-      return false;
-    }
-
-    @Override
-    public int getNumDocs() {
-      return _numDocs;
-    }
-
-    @Override
-    public int getNumValues() {
-      return _numDocs;
-    }
-
-    @Override
-    public int getMaxNumValuesPerMVEntry() {
-      return 0;
-    }
-
-    @Override
-    public int getCardinality() {
-      return 1;
-    }
-
-    @Nullable
-    @Override
-    public Comparable getMinValue() {
-      return null;
-    }
-
-    @Nullable
-    @Override
-    public Comparable getMaxValue() {
-      return null;
-    }
-
-    @Nullable
-    @Override
-    public PartitionFunction getPartitionFunction() {
-      return null;
-    }
-
-    @Nullable
-    @Override
-    public Set<Integer> getPartitions() {
-      return null;
-    }
-  }
-}
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/SparseKeyDataSource.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/SparseKeyDataSource.java
index c83c2500264..6ed39cef11c 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/SparseKeyDataSource.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/SparseKeyDataSource.java
@@ -103,9 +103,10 @@ public class SparseKeyDataSource extends BaseDataSource {
       return node == null ? defaultValue : map.apply(node);
     }
 
-    // TODO: these hardcode the type defaults, so a child with a 
schema-configured defaultNullValue
-    // reads back differently here than on the dense path 
(OpenStructColumnSplitter uses the spec).
-    // Fixing it means threading the spec in and updating MapFilterOperator's 
sentinel check to match.
+    // TODO: Read a document without the key as the child field spec's default 
null value, as
+    //   OpenStructDataSource.getValueFieldSpec does for a key absent from the 
segment, instead of the standard type
+    //   default hardcoded below. MapFilterOperator already refuses the sparse 
JSON fast path for the spec's default.
+    //   See https://github.com/apache/pinot/issues/19466
     @Override
     public int getInt(int docId, ForwardIndexReaderContext context) {
       return orDefault(docId, context, JsonNode::asInt, 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT);
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
index d4d7c9fcc10..d733ef17689 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
@@ -166,6 +166,8 @@ public abstract class 
BaseConstantValueVirtualColumnProvider implements VirtualC
     checkValue(context, value);
     ColumnMetadataImpl.Builder builder = new 
ColumnMetadataImpl.Builder().setFieldSpec(fieldSpec)
         .setTotalDocs(context.getTotalDocCount())
+        // Every document holds exactly one value, also for a multi-value 
field (a single-element entry)
+        .setTotalNumberOfEntries(context.getTotalDocCount())
         .setCardinality(1)
         .setHasDictionary(true);
     if (fieldSpec.isSingleValueField()) {
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
index 3e7cc400dee..031d23efdcc 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
@@ -56,64 +56,142 @@ public class DefaultNullValueVirtualColumnProviderTest {
   @Test
   public void testBuildMetadata() {
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_INT, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_INT).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue((int) 
SV_INT.getDefaultNullValue())
-            .setMaxValue((int) SV_INT.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_INT)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue((int) SV_INT.getDefaultNullValue())
+            .setMaxValue((int) SV_INT.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_LONG, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_LONG).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue((long) 
SV_LONG.getDefaultNullValue())
-            .setMaxValue((long) SV_LONG.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_LONG)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue((long) SV_LONG.getDefaultNullValue())
+            .setMaxValue((long) SV_LONG.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_FLOAT, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_FLOAT).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue((float) 
SV_FLOAT.getDefaultNullValue())
-            .setMaxValue((float) SV_FLOAT.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_FLOAT)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue((float) SV_FLOAT.getDefaultNullValue())
+            .setMaxValue((float) SV_FLOAT.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_DOUBLE, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_DOUBLE).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue((double) 
SV_DOUBLE.getDefaultNullValue())
-            .setMaxValue((double) SV_DOUBLE.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_DOUBLE)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue((double) SV_DOUBLE.getDefaultNullValue())
+            .setMaxValue((double) SV_DOUBLE.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_STRING, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_STRING).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue((String) 
SV_STRING.getDefaultNullValue())
-            .setMaxValue((String) SV_STRING.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_STRING)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue((String) SV_STRING.getDefaultNullValue())
+            .setMaxValue((String) SV_STRING.getDefaultNullValue())
+            .build());
 
     assertEquals(
         new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_STRING_WITH_DEFAULT, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_STRING_WITH_DEFAULT).setTotalDocs(1).setCardinality(1)
-            
.setSorted(true).setHasDictionary(true).setMinValue("default").setMaxValue("default").build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_STRING_WITH_DEFAULT)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue("default")
+            .setMaxValue("default")
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(SV_BYTES, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(SV_BYTES).setTotalDocs(1).setCardinality(1).setSorted(true)
-            .setHasDictionary(true).setMinValue(new ByteArray((byte[]) 
SV_BYTES.getDefaultNullValue()))
-            .setMaxValue(new ByteArray((byte[]) 
SV_BYTES.getDefaultNullValue())).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(SV_BYTES)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(true)
+            .setHasDictionary(true)
+            .setMinValue(new ByteArray((byte[]) 
SV_BYTES.getDefaultNullValue()))
+            .setMaxValue(new ByteArray((byte[]) 
SV_BYTES.getDefaultNullValue()))
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(MV_INT, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(MV_INT).setTotalDocs(1).setCardinality(1).setSorted(false)
-            
.setHasDictionary(true).setMaxNumberOfMultiValues(1).setMinValue((int) 
MV_INT.getDefaultNullValue())
-            .setMaxValue((int) MV_INT.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(MV_INT)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(false)
+            .setHasDictionary(true)
+            .setMaxNumberOfMultiValues(1)
+            .setMinValue((int) MV_INT.getDefaultNullValue())
+            .setMaxValue((int) MV_INT.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(MV_LONG, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(MV_LONG).setTotalDocs(1).setCardinality(1).setSorted(false)
-            
.setHasDictionary(true).setMaxNumberOfMultiValues(1).setMinValue((long) 
MV_LONG.getDefaultNullValue())
-            .setMaxValue((long) MV_LONG.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(MV_LONG)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(false)
+            .setHasDictionary(true)
+            .setMaxNumberOfMultiValues(1)
+            .setMinValue((long) MV_LONG.getDefaultNullValue())
+            .setMaxValue((long) MV_LONG.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(MV_FLOAT, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(MV_FLOAT).setTotalDocs(1).setCardinality(1).setSorted(false)
-            
.setHasDictionary(true).setMaxNumberOfMultiValues(1).setMinValue((float) 
MV_FLOAT.getDefaultNullValue())
-        .setMaxValue((float) MV_FLOAT.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(MV_FLOAT)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(false)
+            .setHasDictionary(true)
+            .setMaxNumberOfMultiValues(1)
+            .setMinValue((float) MV_FLOAT.getDefaultNullValue())
+            .setMaxValue((float) MV_FLOAT.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(MV_DOUBLE, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(MV_DOUBLE).setTotalDocs(1).setCardinality(1).setSorted(false)
-            
.setHasDictionary(true).setMaxNumberOfMultiValues(1).setMinValue((double) 
MV_DOUBLE.getDefaultNullValue())
-            .setMaxValue((double) MV_DOUBLE.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(MV_DOUBLE)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(false)
+            .setHasDictionary(true)
+            .setMaxNumberOfMultiValues(1)
+            .setMinValue((double) MV_DOUBLE.getDefaultNullValue())
+            .setMaxValue((double) MV_DOUBLE.getDefaultNullValue())
+            .build());
 
     assertEquals(new DefaultNullValueVirtualColumnProvider().buildMetadata(new 
VirtualColumnContext(MV_STRING, 1)),
-        new 
ColumnMetadataImpl.Builder().setFieldSpec(MV_STRING).setTotalDocs(1).setCardinality(1).setSorted(false)
-            
.setHasDictionary(true).setMaxNumberOfMultiValues(1).setMinValue((String) 
MV_STRING.getDefaultNullValue())
-            .setMaxValue((String) MV_STRING.getDefaultNullValue()).build());
+        new ColumnMetadataImpl.Builder().setFieldSpec(MV_STRING)
+            .setTotalDocs(1)
+            .setTotalNumberOfEntries(1)
+            .setCardinality(1)
+            .setSorted(false)
+            .setHasDictionary(true)
+            .setMaxNumberOfMultiValues(1)
+            .setMinValue((String) MV_STRING.getDefaultNullValue())
+            .setMaxValue((String) MV_STRING.getDefaultNullValue())
+            .build());
   }
 
   @Test
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSourceTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSourceTest.java
new file mode 100644
index 00000000000..1181d70c70f
--- /dev/null
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/datasource/NullDataSourceTest.java
@@ -0,0 +1,135 @@
+/**
+ * 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.pinot.segment.local.segment.index.datasource;
+
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.segment.spi.index.reader.SortedIndexReader;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+
+/// Covers [NullDataSource], the data source for a column holding no value in 
any document. Asserts it is built
+/// like a default column — a single-entry dictionary over the field's default 
null value, constant sorted or
+/// multi-value readers, and metadata with cardinality one — and that every 
document is additionally marked null.
+public class NullDataSourceTest {
+  private static final int NUM_DOCS = 100;
+
+  @Test
+  public void testBuiltLikeDefaultColumn() {
+    NullDataSource dataSource = new NullDataSource(new DimensionFieldSpec("k", 
DataType.LONG, true), NUM_DOCS);
+
+    DataSourceMetadata metadata = dataSource.getDataSourceMetadata();
+    assertEquals(metadata.getDataType(), DataType.LONG);
+    assertTrue(metadata.isSingleValue());
+    assertTrue(metadata.isSorted());
+    assertEquals(metadata.getNumDocs(), NUM_DOCS);
+    assertEquals(metadata.getNumValues(), NUM_DOCS);
+    assertEquals(metadata.getMaxNumValuesPerMVEntry(), -1);
+    assertEquals(metadata.getCardinality(), 1);
+    assertEquals(metadata.getMinValue(), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+    assertEquals(metadata.getMaxValue(), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+
+    Dictionary dictionary = dataSource.getDictionary();
+    assertNotNull(dictionary);
+    assertEquals(dictionary.length(), 1);
+    assertEquals(dictionary.getLongValue(0), (long) 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+
+    ForwardIndexReader<?> forwardIndex = dataSource.getForwardIndex();
+    assertTrue(forwardIndex.isDictionaryEncoded());
+    assertTrue(forwardIndex.isSingleValue());
+    assertEquals(forwardIndex.getDictId(NUM_DOCS - 1, null), 0);
+
+    SortedIndexReader<?> sortedIndex = (SortedIndexReader<?>) 
dataSource.getInvertedIndex();
+    assertEquals(sortedIndex.getDocIds(0).getLeft(), 0);
+    assertEquals(sortedIndex.getDocIds(0).getRight(), NUM_DOCS - 1);
+
+    assertNull(dataSource.getRangeIndex());
+    assertNull(dataSource.getJsonIndex());
+    assertNull(dataSource.getBloomFilter());
+    assertNull(dataSource.getTextIndex());
+    assertNull(dataSource.getVectorIndex());
+  }
+
+  @Test
+  public void testEveryDocumentIsNull() {
+    NullDataSource dataSource = new NullDataSource(new DimensionFieldSpec("k", 
DataType.INT, true), 50);
+    NullValueVectorReader nullValueVector = dataSource.getNullValueVector();
+    assertNotNull(nullValueVector);
+    for (int i = 0; i < 50; i++) {
+      assertTrue(nullValueVector.isNull(i));
+    }
+    assertEquals(nullValueVector.getNullBitmap().getCardinality(), 50);
+
+    dataSource = new NullDataSource(new DimensionFieldSpec("k", DataType.INT, 
true), 0);
+    
assertEquals(dataSource.getNullValueVector().getNullBitmap().getCardinality(), 
0);
+  }
+
+  @Test
+  public void testDictionaryHoldsDefaultNullValue() {
+    assertEquals(dictionaryValue(DataType.INT), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT);
+    assertEquals(dictionaryValue(DataType.LONG), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+    assertEquals(dictionaryValue(DataType.FLOAT), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_FLOAT);
+    assertEquals(dictionaryValue(DataType.DOUBLE), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_DOUBLE);
+    assertEquals(dictionaryValue(DataType.BIG_DECIMAL), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BIG_DECIMAL);
+    assertEquals(dictionaryValue(DataType.BOOLEAN), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BOOLEAN);
+    assertEquals(dictionaryValue(DataType.TIMESTAMP), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+    assertEquals(dictionaryValue(DataType.STRING), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_STRING);
+    assertEquals(dictionaryValue(DataType.JSON), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_JSON);
+    assertEquals(dictionaryValue(DataType.BYTES), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BYTES);
+  }
+
+  @Test
+  public void testCustomDefaultNullValue() {
+    NullDataSource dataSource = new NullDataSource(new DimensionFieldSpec("k", 
DataType.STRING, true, "N/A"), NUM_DOCS);
+    assertEquals(dataSource.getDictionary().get(0), "N/A");
+    assertEquals(dataSource.getDataSourceMetadata().getMinValue(), "N/A");
+    assertEquals(dataSource.getDataSourceMetadata().getMaxValue(), "N/A");
+  }
+
+  @Test
+  public void testMultiValue() {
+    NullDataSource dataSource = new NullDataSource(new DimensionFieldSpec("k", 
DataType.INT, false), NUM_DOCS);
+
+    DataSourceMetadata metadata = dataSource.getDataSourceMetadata();
+    assertFalse(metadata.isSingleValue());
+    assertFalse(metadata.isSorted());
+    assertEquals(metadata.getNumValues(), NUM_DOCS);
+    assertEquals(metadata.getMaxNumValuesPerMVEntry(), 1);
+
+    ForwardIndexReader<?> forwardIndex = dataSource.getForwardIndex();
+    assertTrue(forwardIndex.isDictionaryEncoded());
+    assertFalse(forwardIndex.isSingleValue());
+    assertEquals(forwardIndex.getNumValuesMV(0, null), 1);
+    assertEquals(forwardIndex.getDictIdMV(0, null), new int[]{0});
+    ImmutableRoaringBitmap docIds = (ImmutableRoaringBitmap) 
dataSource.getInvertedIndex().getDocIds(0);
+    assertEquals(docIds.getCardinality(), NUM_DOCS);
+  }
+
+  private static Object dictionaryValue(DataType dataType) {
+    return new NullDataSource(new DimensionFieldSpec("k", dataType, true), 
NUM_DOCS).getDictionary().get(0);
+  }
+}
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSourceTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSourceTest.java
new file mode 100644
index 00000000000..409893bbb25
--- /dev/null
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/BaseMapDataSourceTest.java
@@ -0,0 +1,71 @@
+/**
+ * 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.pinot.segment.local.segment.index.map;
+
+import java.util.Map;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
+import org.apache.pinot.segment.spi.index.reader.MapIndexReader;
+import org.apache.pinot.spi.data.ComplexFieldSpec;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.testng.annotations.Test;
+
+import static org.apache.pinot.spi.data.ComplexFieldSpec.KEY_FIELD;
+import static org.apache.pinot.spi.data.ComplexFieldSpec.VALUE_FIELD;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// Covers [BaseMapDataSource#getDataSource] on the branch taken when the map 
index reports the key absent: the
+/// key must resolve to an all-null [NullDataSource] typed as the map's value 
field and spanning the doc count.
+public class BaseMapDataSourceTest {
+  private static final int NUM_DOCS = 100;
+
+  @Test
+  public void testAbsentKeyResolvesToAllNullSourceOfValueType() {
+    ComplexFieldSpec mapFieldSpec = new ComplexFieldSpec("m", DataType.MAP, 
true, Map.of(
+        KEY_FIELD, new DimensionFieldSpec(KEY_FIELD, DataType.STRING, true),
+        VALUE_FIELD, new DimensionFieldSpec(VALUE_FIELD, DataType.LONG, true)
+    ));
+    DataSourceMetadata mapMetadata = mock(DataSourceMetadata.class);
+    when(mapMetadata.getFieldSpec()).thenReturn(mapFieldSpec);
+    when(mapMetadata.getNumDocs()).thenReturn(NUM_DOCS);
+    MapIndexReader<?> mapIndexReader = mock(MapIndexReader.class);
+    when(mapIndexReader.getIndexes("absent")).thenReturn(null);
+    BaseMapDataSource mapDataSource = new BaseMapDataSource(mapMetadata, 
ColumnIndexContainer.Empty.INSTANCE) {
+      @Override
+      public MapIndexReader<?> getMapIndexReader() {
+        return mapIndexReader;
+      }
+    };
+
+    DataSource keyDataSource = mapDataSource.getDataSource("absent");
+    assertTrue(keyDataSource instanceof NullDataSource);
+    DataSourceMetadata metadata = keyDataSource.getDataSourceMetadata();
+    assertEquals(metadata.getDataType(), DataType.LONG);
+    assertEquals(metadata.getNumDocs(), NUM_DOCS);
+    assertEquals(keyDataSource.getDictionary().get(0), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+  }
+}
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSourceTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSourceTest.java
index 8fcd0cf8090..4572ad11207 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSourceTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSourceTest.java
@@ -20,6 +20,7 @@ package 
org.apache.pinot.segment.local.segment.index.openstruct;
 
 import java.util.List;
 import java.util.Map;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
 import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
@@ -31,12 +32,7 @@ import org.testng.annotations.Test;
 
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-import static org.testng.Assert.assertSame;
-import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.*;
 
 
 public class ImmutableOpenStructDataSourceTest {
@@ -62,8 +58,26 @@ public class ImmutableOpenStructDataSourceTest {
         null);
 
     assertSame(ds.getDataSource("clicks"), clicksDs);
-    // absent key with mock sparse (no real forward index) returns null
-    assertNull(ds.getDataSource("unknown"));
+    // Absent key with mock sparse (no real forward index) resolves to an 
all-null STRING source
+    DataSource unknownDs = ds.getDataSource("unknown");
+    assertTrue(unknownDs instanceof NullDataSource);
+    assertEquals(unknownDs.getDataSourceMetadata().getDataType(), 
DataType.STRING);
+  }
+
+  /// A key absent from the whole segment is described by its declared child 
field spec, including a custom default
+  /// null value.
+  @Test
+  public void testAbsentDeclaredKeyReadsDeclaredDefault() {
+    ComplexFieldSpec spec = new ComplexFieldSpec("event", 
DataType.OPEN_STRUCT, true,
+        Map.of("score", new DimensionFieldSpec("score", DataType.STRING, true, 
"N/A")));
+    ImmutableOpenStructDataSource ds = new ImmutableOpenStructDataSource(spec, 
Map.of(), null, 7, null);
+
+    DataSource scoreDs = ds.getDataSource("score");
+    assertTrue(scoreDs instanceof NullDataSource);
+    assertEquals(scoreDs.getDataSourceMetadata().getDataType(), 
DataType.STRING);
+    assertEquals(scoreDs.getDataSourceMetadata().getNumDocs(), 7);
+    assertEquals(scoreDs.getDictionary().get(0), "N/A");
+    
assertEquals(scoreDs.getNullValueVector().getNullBitmap().getCardinality(), 7);
   }
 
   @Test
@@ -153,7 +167,7 @@ public class ImmutableOpenStructDataSourceTest {
         null);
 
     assertSame(ds.getDataSourceMetadata("clicks"), clicksMeta);
-    assertNull(ds.getDataSourceMetadata("absent"));
+    assertEquals(ds.getDataSourceMetadata("absent").getDataType(), 
DataType.STRING);
   }
 
   @Test
@@ -235,7 +249,7 @@ public class ImmutableOpenStructDataSourceTest {
     assertTrue(regionDs instanceof SparseKeyDataSource);
     assertEquals(regionDs.getForwardIndex().getString(0, null), "us");
     assertSame(ds.getDataSource("region"), regionDs);
-    assertNull(ds.getDataSource("nope"));
+    assertTrue(ds.getDataSource("nope") instanceof NullDataSource);
     assertFalse(ds.isMaterialized("region"));
     assertFalse(ds.isFullyMaterialized());
   }
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSourceTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSourceTest.java
index 271490e850b..aa2c67b5410 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSourceTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSourceTest.java
@@ -21,6 +21,7 @@ package 
org.apache.pinot.segment.local.segment.index.openstruct;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.pinot.segment.local.io.writer.impl.DirectMemoryManager;
+import org.apache.pinot.segment.local.segment.index.datasource.NullDataSource;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
 import org.apache.pinot.segment.spi.memory.PinotDataBufferMemoryManager;
@@ -36,12 +37,7 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-import static org.testng.Assert.assertSame;
-import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.*;
 
 
 public class MutableOpenStructDataSourceTest {
@@ -83,11 +79,29 @@ public class MutableOpenStructDataSourceTest {
     try (MutableOpenStructIndex idx = new MutableOpenStructIndex("metrics", 
"testTable_REALTIME", spec(),
         OpenStructIndexConfig.DEFAULT, _mm, 100)) {
       MutableOpenStructDataSource ds = new MutableOpenStructDataSource(spec(), 
idx, 0);
-      assertNull(ds.getDataSource("missing"));
+      assertTrue(ds.getDataSource("missing") instanceof NullDataSource);
       assertFalse(ds.isMaterialized("missing"));
     }
   }
 
+  /// A key absent from the consuming segment is described by its declared 
child field spec, including a custom
+  /// default null value.
+  @Test
+  public void testAbsentDeclaredKeyReadsDeclaredDefault()
+      throws Exception {
+    ComplexFieldSpec spec = new ComplexFieldSpec("metrics", 
DataType.OPEN_STRUCT, true,
+        Map.of("score", new DimensionFieldSpec("score", DataType.STRING, true, 
"N/A")));
+    try (MutableOpenStructIndex idx = new MutableOpenStructIndex("metrics", 
"testTable_REALTIME", spec,
+        OpenStructIndexConfig.DEFAULT, _mm, 100)) {
+      MutableOpenStructDataSource ds = new MutableOpenStructDataSource(spec, 
idx, 3);
+      DataSource scoreDs = ds.getDataSource("score");
+      assertTrue(scoreDs instanceof NullDataSource);
+      assertEquals(scoreDs.getDataSourceMetadata().getDataType(), 
DataType.STRING);
+      assertEquals(scoreDs.getDataSourceMetadata().getNumDocs(), 3);
+      assertEquals(scoreDs.getDictionary().get(0), "N/A");
+    }
+  }
+
   @Test
   public void testGetDataSourcesReturnsAllKeys()
       throws Exception {
@@ -155,9 +169,9 @@ public class MutableOpenStructDataSourceTest {
     try (MutableOpenStructIndex idx = new MutableOpenStructIndex("metrics", 
"testTable_REALTIME", spec(),
         OpenStructIndexConfig.DEFAULT, _mm, 100)) {
       MutableOpenStructDataSource ds = new MutableOpenStructDataSource(spec(), 
idx, 1);
-      assertNull(ds.getDataSource("clicks"));
+      assertTrue(ds.getDataSource("clicks") instanceof NullDataSource);
       idx.index(0, Map.of("clicks", 5L));
-      assertNotNull(ds.getDataSource("clicks"));
+      assertFalse(ds.getDataSource("clicks") instanceof NullDataSource);
     }
   }
 
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSourceTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSourceTest.java
deleted file mode 100644
index 7dcd20d7ef2..00000000000
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSourceTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * 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.pinot.segment.local.segment.index.openstruct;
-
-import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
-import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
-import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
-import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
-import org.testng.annotations.Test;
-
-import static org.testng.Assert.*;
-
-
-public class OpenStructNullDataSourceTest {
-
-  @Test
-  public void testMetadataReportsCorrectTypeAndNumDocs() {
-    FieldSpec spec = new DimensionFieldSpec("region", 
FieldSpec.DataType.STRING, true);
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(spec, 100);
-    DataSourceMetadata metadata = ds.getDataSourceMetadata();
-
-    assertEquals(metadata.getFieldSpec().getDataType(), 
FieldSpec.DataType.STRING);
-    assertEquals(metadata.getNumDocs(), 100);
-    assertEquals(metadata.getCardinality(), 1);
-  }
-
-  @Test
-  public void testNullValueVectorAllDocsNull() {
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(
-        new DimensionFieldSpec("k", FieldSpec.DataType.INT, true), 50);
-    NullValueVectorReader nullReader = ds.getNullValueVector();
-
-    assertNotNull(nullReader);
-    for (int i = 0; i < 50; i++) {
-      assertTrue(nullReader.isNull(i));
-    }
-    ImmutableRoaringBitmap bitmap = nullReader.getNullBitmap();
-    assertEquals(bitmap.getCardinality(), 50);
-  }
-
-  @Test
-  public void testNullValueVectorZeroDocs() {
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(
-        new DimensionFieldSpec("k", FieldSpec.DataType.INT, true), 0);
-    NullValueVectorReader nullReader = ds.getNullValueVector();
-    assertEquals(nullReader.getNullBitmap().getCardinality(), 0);
-  }
-
-  @Test
-  public void testForwardIndexReturnsDefaultNullValues() {
-    // INT
-    verifyForwardIndex(FieldSpec.DataType.INT, 10, (fwd) -> {
-      assertEquals(fwd.getInt(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_INT);
-      assertEquals(fwd.getStoredType(), FieldSpec.DataType.INT);
-    });
-    // LONG
-    verifyForwardIndex(FieldSpec.DataType.LONG, 10, (fwd) -> {
-      assertEquals(fwd.getLong(0, null), (long) 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
-    });
-    // FLOAT
-    verifyForwardIndex(FieldSpec.DataType.FLOAT, 10, (fwd) -> {
-      assertEquals(fwd.getFloat(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_FLOAT);
-    });
-    // DOUBLE
-    verifyForwardIndex(FieldSpec.DataType.DOUBLE, 10, (fwd) -> {
-      assertEquals(fwd.getDouble(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_DOUBLE);
-    });
-    // STRING
-    verifyForwardIndex(FieldSpec.DataType.STRING, 10, (fwd) -> {
-      assertEquals(fwd.getString(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_STRING);
-      assertEquals(fwd.getStoredType(), FieldSpec.DataType.STRING);
-    });
-    // BYTES
-    verifyForwardIndex(FieldSpec.DataType.BYTES, 10, (fwd) -> {
-      assertEquals(fwd.getBytes(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BYTES);
-    });
-    // BIG_DECIMAL
-    verifyForwardIndex(FieldSpec.DataType.BIG_DECIMAL, 10, (fwd) -> {
-      assertEquals(fwd.getBigDecimal(0, null), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_BIG_DECIMAL);
-    });
-  }
-
-  @Test
-  public void testForwardIndexIsSingleValueNotDictEncoded() {
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(
-        new DimensionFieldSpec("k", FieldSpec.DataType.STRING, true), 5);
-    ForwardIndexReader<?> fwd = ds.getForwardIndex();
-    assertTrue(fwd.isSingleValue());
-    assertFalse(fwd.isDictionaryEncoded());
-  }
-
-  @Test
-  public void testNoDictionaryOrSecondaryIndexes() {
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(
-        new DimensionFieldSpec("k", FieldSpec.DataType.STRING, true), 5);
-    assertNull(ds.getDictionary());
-    assertNull(ds.getInvertedIndex());
-    assertNull(ds.getRangeIndex());
-    assertNull(ds.getJsonIndex());
-    assertNull(ds.getBloomFilter());
-    assertNull(ds.getTextIndex());
-    assertNull(ds.getVectorIndex());
-  }
-
-  @FunctionalInterface
-  private interface ForwardIndexAssertion {
-    void check(ForwardIndexReader<?> fwd);
-  }
-
-  private void verifyForwardIndex(FieldSpec.DataType type, int numDocs, 
ForwardIndexAssertion assertion) {
-    OpenStructNullDataSource ds = new OpenStructNullDataSource(
-        new DimensionFieldSpec("k", type, true), numDocs);
-    assertion.check(ds.getForwardIndex());
-  }
-}
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/MapDataSource.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/MapDataSource.java
index 292043450ab..4458fd5f3d8 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/MapDataSource.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/MapDataSource.java
@@ -30,7 +30,8 @@ public interface MapDataSource extends DataSource {
   /// Returns the map FieldSpec.
   ComplexFieldSpec.MapFieldSpec getFieldSpec();
 
-  /// Returns the DataSource for the given map key's values.
+  /// Returns the DataSource for the given map key's values. A key absent from 
the map resolves to an all-null
+  /// DataSource typed as the map's value field, so the result is never `null`.
   DataSource getDataSource(String key);
 
   /// Returns DataSources for all keys present in this segment.
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/OpenStructDataSource.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/OpenStructDataSource.java
index 5879402ebb7..3be2f7f35aa 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/OpenStructDataSource.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/datasource/OpenStructDataSource.java
@@ -23,6 +23,9 @@ import javax.annotation.Nullable;
 import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
 import org.apache.pinot.segment.spi.index.reader.JsonIndexReader;
 import org.apache.pinot.spi.data.ComplexFieldSpec;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
 
 
 /// DataSource for an OPEN_STRUCT column. Provides per-key DataSources that 
can be used for
@@ -33,21 +36,23 @@ public interface OpenStructDataSource extends DataSource {
   /// Returns the OPEN_STRUCT ComplexFieldSpec.
   ComplexFieldSpec getFieldSpec();
 
-  /// Returns the DataSource for the given key's values. The DataSource's 
value type is the
-  /// per-key declared type (from `childFieldSpecs`) when present, otherwise 
STRING.
-  ///
-  /// Three possible outcomes:
+  /// Returns the DataSource for the given key's values, typed per 
[#getValueFieldSpec(String)]. Never `null`:
   /// - **Materialized key** → the dense per-key DataSource (has dictionary / 
inverted index).
-  /// - **Sparse key** → a virtual blob-backed DataSource that parses the 
sparse JSON column
-  ///   per doc and coerces values to the resolved stored type. Returned when 
the key is inside
-  ///   the sparse manifest, or when there is no manifest (pre-manifest 
segments treat every
-  ///   unmaterialized key as potentially sparse).
-  /// - **Definitively absent** → `null`. Returned when the segment is fully 
materialized (no
-  ///   sparse column), or when the sparse manifest exists and does not list 
this key. Callers
-  ///   should synthesize a typed all-null source (see 
`OpenStructNullDataSource.forAbsentKey`).
-  @Nullable
+  /// - **Sparse key** → a virtual blob-backed DataSource that parses the 
sparse JSON column per doc and coerces
+  ///   values to the resolved stored type. Returned when the key is inside 
the sparse manifest, or when there is no
+  ///   manifest (pre-manifest segments treat every unmaterialized key as 
potentially sparse).
+  /// - **Definitively absent** → an all-null DataSource built like a default 
column: every document is null, and
+  ///   reads as the key's default null value with null handling off. Returned 
when the segment is fully materialized
+  ///   (no sparse column), or when the sparse manifest exists and does not 
list this key.
   DataSource getDataSource(String key);
 
+  /// Returns the field spec describing the given key's values: the declared 
child field spec when present, otherwise a
+  /// single-value STRING dimension named after the key.
+  default FieldSpec getValueFieldSpec(String key) {
+    FieldSpec childFieldSpec = getFieldSpec().getChildFieldSpec(key);
+    return childFieldSpec != null ? childFieldSpec : new 
DimensionFieldSpec(key, DataType.STRING, true);
+  }
+
   /// Returns whether the given key has a materialized per-key index in this 
segment. Exact,
   /// O(1) lookup into the materialized key set. Virtual sparse-backed sources 
do not count as
   /// materialized.
@@ -57,32 +62,27 @@ public interface OpenStructDataSource extends DataSource {
   /// a non-materialized key resolves to a typed all-null source, so the scan 
sees NULL at every
   /// document.
   ///
-  /// A `false` return combined with a non-null [#getDataSource(String)] means 
the key exists
-  /// in the sparse tier. A `false` return with a null getDataSource means the 
key is definitively
-  /// absent.
+  /// A `false` return means the key is either in the sparse tier or 
definitively absent; [#getDataSource(String)]
+  /// returns the matching source either way.
   boolean isMaterialized(String key);
 
   /// Returns whether every key in this segment is materialized — i.e., there 
is no sparse
   /// blob and the materialized key set is exhaustive.
   ///
-  /// When `true`, a `false` return from [#isMaterialized(String)] is a 
definitive "absent"
-  /// and callers can treat the key as present-but-all-null — e.g. evaluate 
predicates against a
-  /// typed all-null DataSource, which yields the correct answer under both 
null-handling modes
-  /// (an absent key reads as its type default with null handling off, and as 
NULL with it on).
+  /// When `true`, a `false` return from [#isMaterialized(String)] is a 
definitive "absent", and
+  /// [#getDataSource(String)] returns an all-null DataSource for the key, 
which yields the correct answer under both
+  /// null-handling modes (an absent key reads as its type default with null 
handling off, and as NULL with it on).
   boolean isFullyMaterialized();
 
   /// Returns DataSources for all keys present in this segment.
   Map<String, DataSource> getDataSources();
 
-  /// Returns the DataSourceMetadata for the given key, or `null` when 
[#getDataSource(String)]
-  /// returns null for this key. Includes metadata from virtual sparse-backed 
sources.
-  @Nullable
+  /// Returns the DataSourceMetadata for the given key, i.e. that of 
[#getDataSource(String)].
   DataSourceMetadata getDataSourceMetadata(String key);
 
-  /// Returns the ColumnIndexContainer for the given key, or `null` when the 
key has no
-  /// materialized DataSource. Virtual sparse-backed sources do not expose a 
ColumnIndexContainer
-  /// (they use BaseDataSource, not ImmutableDataSource), so this returns null 
for sparse keys
-  /// even when [#getDataSource(String)] is non-null.
+  /// Returns the ColumnIndexContainer for the given key, or `null` for a 
sparse key: virtual sparse-backed sources do
+  /// not expose a ColumnIndexContainer (they use BaseDataSource, not 
ImmutableDataSource). A key absent from the
+  /// segment resolves to an all-null DataSource and returns its container.
   @Nullable
   ColumnIndexContainer getIndexContainer(String key);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to