raghavyadav01 commented on code in PR #19093:
URL: https://github.com/apache/pinot/pull/19093#discussion_r4018897684


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java:
##########
@@ -140,6 +144,141 @@ public JsonIndexReader getSparseJsonIndex() {
     return _sparseDataSource != null ? _sparseDataSource.getJsonIndex() : null;
   }
 
+  @SuppressWarnings("unchecked")
+  @Nullable
+  @Override
+  public Map<String, Object> getMapValue(int docId) {
+    Map<String, Object> result = null;
+
+    for (Map.Entry<String, DataSource> entry : _perKeyDataSources.entrySet()) {
+      Object value = readValue(entry.getKey(), entry.getValue(), docId);
+      if (value != null) {
+        if (result == null) {
+          result = new HashMap<>();
+        }
+        result.put(entry.getKey(), value);
+      }
+    }
+
+    if (_sparseDataSource != null) {
+      Object sparseValue = readValue(_fieldSpec.getName(), _sparseDataSource, 
docId);
+      if (sparseValue instanceof String) {
+        String json = (String) sparseValue;
+        if (!json.isEmpty()) {
+          try {
+            Map<String, Object> sparseMap = JsonUtils.stringToObject(json, 
Map.class);
+            if (result == null) {
+              result = new HashMap<>();
+            }
+            result.putAll(sparseMap);
+          } catch (IOException e) {
+            throw new RuntimeException("Failed to parse sparse JSON at docId " 
+ docId, e);
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /// Reads the value of `key` at `docId`, or `null` when the doc is null or 
the column has no
+  /// forward index. Delegates the null-vector check and the dictionary/raw 
per-type read dispatch to
+  /// [PinotSegmentColumnReader] rather than re-deriving them here, so this 
path cannot drift from the
+  /// reader every other column read in the engine already goes through. 
OPEN_STRUCT child columns are
+  /// always single-valued, hence the 0 maxNumValuesPerMVEntry.
+  @Nullable
+  private static Object readValue(String key, DataSource dataSource, int 
docId) {
+    ForwardIndexReader<?> fwdReader = dataSource.getForwardIndex();
+    if (fwdReader == null) {
+      return null;
+    }
+    try (PinotSegmentColumnReader reader = new PinotSegmentColumnReader(key, 
fwdReader, dataSource.getDictionary(),
+        dataSource.getNullValueVector(), 0)) {
+      return reader.isNull(docId) ? null : reader.getValue(docId);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to read value from OPEN_STRUCT key 
forward index", e);
+    }
+  }
+
+  @Override
+  public MapValueReader openMapValueReader() {
+    return new CachingMapValueReader();
+  }
+
+  /// Caches one [PinotSegmentColumnReader] per key for the life of the 
reader, instead of the
+  /// per-call construct-and-close [#readValue]/[#getMapValue] does. For a 
raw, chunk-compressed
+  /// column (e.g. the sparse blob), a fresh reader per doc means a fresh 
decompression buffer and,
+  /// depending on access order, redundant re-decompression of the same chunk; 
reusing the reader
+  /// across a sequential scan lets it carry its decoded-chunk state forward. 
Not thread-safe — for
+  /// one single-threaded scan only, per 
[OpenStructDataSource#openMapValueReader()].
+  private final class CachingMapValueReader implements MapValueReader {
+    private final Map<String, PinotSegmentColumnReader> _readers = new 
HashMap<>();
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    @Override
+    public Map<String, Object> getMapValue(int docId) {
+      Map<String, Object> result = null;
+
+      for (Map.Entry<String, DataSource> entry : 
_perKeyDataSources.entrySet()) {
+        Object value = readValue(entry.getKey(), entry.getValue(), docId);
+        if (value != null) {
+          if (result == null) {
+            result = new HashMap<>();
+          }
+          result.put(entry.getKey(), value);
+        }
+      }
+
+      if (_sparseDataSource != null) {
+        Object sparseValue = readValue(_fieldSpec.getName(), 
_sparseDataSource, docId);

Review Comment:
   The reader cache is keyed by child key name, but the sparse blob is cached 
under the parent column name. A child key that matches the parent (`{"event": 
{"event": 1, "region": "us"}}`) hits the dense reader here, so `instanceof 
String` fails and every sparse key is silently dropped. Could the sparse reader 
get its own field instead of sharing the map?



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java:
##########
@@ -140,6 +144,141 @@ public JsonIndexReader getSparseJsonIndex() {
     return _sparseDataSource != null ? _sparseDataSource.getJsonIndex() : null;
   }
 
+  @SuppressWarnings("unchecked")
+  @Nullable
+  @Override
+  public Map<String, Object> getMapValue(int docId) {
+    Map<String, Object> result = null;
+
+    for (Map.Entry<String, DataSource> entry : _perKeyDataSources.entrySet()) {
+      Object value = readValue(entry.getKey(), entry.getValue(), docId);
+      if (value != null) {
+        if (result == null) {
+          result = new HashMap<>();
+        }
+        result.put(entry.getKey(), value);
+      }
+    }
+
+    if (_sparseDataSource != null) {
+      Object sparseValue = readValue(_fieldSpec.getName(), _sparseDataSource, 
docId);
+      if (sparseValue instanceof String) {
+        String json = (String) sparseValue;
+        if (!json.isEmpty()) {
+          try {
+            Map<String, Object> sparseMap = JsonUtils.stringToObject(json, 
Map.class);
+            if (result == null) {
+              result = new HashMap<>();
+            }
+            result.putAll(sparseMap);
+          } catch (IOException e) {
+            throw new RuntimeException("Failed to parse sparse JSON at docId " 
+ docId, e);
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /// Reads the value of `key` at `docId`, or `null` when the doc is null or 
the column has no
+  /// forward index. Delegates the null-vector check and the dictionary/raw 
per-type read dispatch to
+  /// [PinotSegmentColumnReader] rather than re-deriving them here, so this 
path cannot drift from the
+  /// reader every other column read in the engine already goes through. 
OPEN_STRUCT child columns are
+  /// always single-valued, hence the 0 maxNumValuesPerMVEntry.
+  @Nullable
+  private static Object readValue(String key, DataSource dataSource, int 
docId) {
+    ForwardIndexReader<?> fwdReader = dataSource.getForwardIndex();
+    if (fwdReader == null) {
+      return null;
+    }
+    try (PinotSegmentColumnReader reader = new PinotSegmentColumnReader(key, 
fwdReader, dataSource.getDictionary(),
+        dataSource.getNullValueVector(), 0)) {
+      return reader.isNull(docId) ? null : reader.getValue(docId);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to read value from OPEN_STRUCT key 
forward index", e);
+    }
+  }
+
+  @Override
+  public MapValueReader openMapValueReader() {
+    return new CachingMapValueReader();
+  }
+
+  /// Caches one [PinotSegmentColumnReader] per key for the life of the 
reader, instead of the
+  /// per-call construct-and-close [#readValue]/[#getMapValue] does. For a 
raw, chunk-compressed
+  /// column (e.g. the sparse blob), a fresh reader per doc means a fresh 
decompression buffer and,
+  /// depending on access order, redundant re-decompression of the same chunk; 
reusing the reader
+  /// across a sequential scan lets it carry its decoded-chunk state forward. 
Not thread-safe — for
+  /// one single-threaded scan only, per 
[OpenStructDataSource#openMapValueReader()].
+  private final class CachingMapValueReader implements MapValueReader {
+    private final Map<String, PinotSegmentColumnReader> _readers = new 
HashMap<>();
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    @Override
+    public Map<String, Object> getMapValue(int docId) {

Review Comment:
   This is the same body as `getMapValue` above, and the two copies have 
already drifted — the cached `readValue` dropped the exception wrapper that 
adds read context. Could the outer one just delegate: `try (MapValueReader r = 
openMapValueReader()) { return r.getMapValue(docId); }`?



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructIndexType.java:
##########
@@ -88,6 +89,36 @@ public void validate(FieldIndexConfigs indexConfigs, 
FieldSpec fieldSpec, TableC
           fieldSpec.getName());
       validatePerKeyIndexes(config);
       validateIgnoredKeys(config, fieldSpec);
+      if (fieldSpec instanceof ComplexFieldSpec) {
+        validateChildFieldSpecTypes((ComplexFieldSpec) fieldSpec);
+      }
+    }
+  }
+
+  /// Rejects a declared child key type that the consuming and sealed-build 
paths cannot coerce a
+  /// value to (e.g. STRUCT, LIST, nested OPEN_STRUCT, UNKNOWN — 
[ColumnDataType#fromDataTypeSV] or
+  /// the resulting [ColumnDataType#toPinotDataType] throws for these). 
Catching this at config
+  /// validation, rather than surfacing it as an uncaught exception on the 
first row ingested for
+  /// such a key, keeps a bad declared type from taking down the whole 
consuming thread.
+  private void validateChildFieldSpecTypes(ComplexFieldSpec fieldSpec) {
+    Map<String, FieldSpec> childFieldSpecs = fieldSpec.getChildFieldSpecs();
+    if (childFieldSpecs == null) {
+      return;
+    }
+    for (Map.Entry<String, FieldSpec> entry : childFieldSpecs.entrySet()) {
+      FieldSpec.DataType storedType = 
entry.getValue().getDataType().getStoredType();
+      Preconditions.checkState(isCoercible(storedType),
+          "OPEN_STRUCT column '%s': child key '%s' declares type '%s', which 
cannot be coerced for indexing",
+          fieldSpec.getName(), entry.getKey(), storedType);
+    }
+  }
+
+  private static boolean isCoercible(FieldSpec.DataType storedType) {
+    try {

Review Comment:
   `fromDataTypeSV` maps MAP to MAP and OPEN_STRUCT to OBJECT, and 
`toPinotDataType()` accepts both, so a child declared as either passes this 
check. It then reaches `allocateKeyColumn`, where 
`FieldSpec.getDefaultNullValue(DIMENSION, MAP, null)` throws uncaught on the 
consuming thread — the same failure this was meant to close. Worth restricting 
to the types a key column can actually store?



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java:
##########
@@ -140,6 +144,141 @@ public JsonIndexReader getSparseJsonIndex() {
     return _sparseDataSource != null ? _sparseDataSource.getJsonIndex() : null;
   }
 
+  @SuppressWarnings("unchecked")
+  @Nullable
+  @Override
+  public Map<String, Object> getMapValue(int docId) {
+    Map<String, Object> result = null;
+
+    for (Map.Entry<String, DataSource> entry : _perKeyDataSources.entrySet()) {
+      Object value = readValue(entry.getKey(), entry.getValue(), docId);
+      if (value != null) {
+        if (result == null) {
+          result = new HashMap<>();
+        }
+        result.put(entry.getKey(), value);
+      }
+    }
+
+    if (_sparseDataSource != null) {
+      Object sparseValue = readValue(_fieldSpec.getName(), _sparseDataSource, 
docId);
+      if (sparseValue instanceof String) {
+        String json = (String) sparseValue;
+        if (!json.isEmpty()) {
+          try {
+            Map<String, Object> sparseMap = JsonUtils.stringToObject(json, 
Map.class);
+            if (result == null) {
+              result = new HashMap<>();
+            }
+            result.putAll(sparseMap);
+          } catch (IOException e) {
+            throw new RuntimeException("Failed to parse sparse JSON at docId " 
+ docId, e);
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /// Reads the value of `key` at `docId`, or `null` when the doc is null or 
the column has no
+  /// forward index. Delegates the null-vector check and the dictionary/raw 
per-type read dispatch to
+  /// [PinotSegmentColumnReader] rather than re-deriving them here, so this 
path cannot drift from the
+  /// reader every other column read in the engine already goes through. 
OPEN_STRUCT child columns are
+  /// always single-valued, hence the 0 maxNumValuesPerMVEntry.
+  @Nullable
+  private static Object readValue(String key, DataSource dataSource, int 
docId) {
+    ForwardIndexReader<?> fwdReader = dataSource.getForwardIndex();
+    if (fwdReader == null) {
+      return null;
+    }
+    try (PinotSegmentColumnReader reader = new PinotSegmentColumnReader(key, 
fwdReader, dataSource.getDictionary(),
+        dataSource.getNullValueVector(), 0)) {
+      return reader.isNull(docId) ? null : reader.getValue(docId);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to read value from OPEN_STRUCT key 
forward index", e);
+    }
+  }
+
+  @Override
+  public MapValueReader openMapValueReader() {
+    return new CachingMapValueReader();
+  }
+
+  /// Caches one [PinotSegmentColumnReader] per key for the life of the 
reader, instead of the
+  /// per-call construct-and-close [#readValue]/[#getMapValue] does. For a 
raw, chunk-compressed
+  /// column (e.g. the sparse blob), a fresh reader per doc means a fresh 
decompression buffer and,
+  /// depending on access order, redundant re-decompression of the same chunk; 
reusing the reader
+  /// across a sequential scan lets it carry its decoded-chunk state forward. 
Not thread-safe — for
+  /// one single-threaded scan only, per 
[OpenStructDataSource#openMapValueReader()].
+  private final class CachingMapValueReader implements MapValueReader {
+    private final Map<String, PinotSegmentColumnReader> _readers = new 
HashMap<>();
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    @Override
+    public Map<String, Object> getMapValue(int docId) {
+      Map<String, Object> result = null;
+
+      for (Map.Entry<String, DataSource> entry : 
_perKeyDataSources.entrySet()) {
+        Object value = readValue(entry.getKey(), entry.getValue(), docId);
+        if (value != null) {
+          if (result == null) {
+            result = new HashMap<>();
+          }
+          result.put(entry.getKey(), value);
+        }
+      }
+
+      if (_sparseDataSource != null) {
+        Object sparseValue = readValue(_fieldSpec.getName(), 
_sparseDataSource, docId);
+        if (sparseValue instanceof String) {
+          String json = (String) sparseValue;
+          if (!json.isEmpty()) {
+            try {
+              Map<String, Object> sparseMap = JsonUtils.stringToObject(json, 
Map.class);
+              if (result == null) {
+                result = new HashMap<>();
+              }
+              result.putAll(sparseMap);
+            } catch (IOException e) {
+              throw new RuntimeException("Failed to parse sparse JSON at docId 
" + docId, e);
+            }
+          }
+        }
+      }
+
+      return result;
+    }
+
+    @Nullable
+    private Object readValue(String key, DataSource dataSource, int docId) {
+      PinotSegmentColumnReader reader = _readers.computeIfAbsent(key, k -> 
createReader(k, dataSource));
+      if (reader == null) {
+        return null;
+      }
+      return reader.isNull(docId) ? null : reader.getValue(docId);
+    }
+
+    @Nullable
+    private PinotSegmentColumnReader createReader(String key, DataSource 
dataSource) {
+      ForwardIndexReader<?> fwdReader = dataSource.getForwardIndex();
+      if (fwdReader == null) {
+        return null;
+      }
+      return new PinotSegmentColumnReader(key, fwdReader, 
dataSource.getDictionary(),
+          dataSource.getNullValueVector(), 0);
+    }
+
+    @Override
+    public void close()
+        throws IOException {
+      for (PinotSegmentColumnReader reader : _readers.values()) {

Review Comment:
   If one reader's `close()` throws, the rest leak their forward-index 
contexts. Same shape in `PinotSegmentRecordReader#close`, where a column-reader 
failure skips the map-value readers entirely. Worth collecting and suppressing 
so the loop always finishes?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to