tarun11Mavani commented on code in PR #19608:
URL: https://github.com/apache/pinot/pull/19608#discussion_r4060770703


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java:
##########
@@ -196,62 +212,125 @@ public Set<String> classify() {
 
   private void addMap(@Nullable Map<String, Object> map) {
     if (map != null && !map.isEmpty()) {
-      for (Map.Entry<String, Object> entry : map.entrySet()) {
-        String key = entry.getKey();
-        Object rawValue = entry.getValue();
-        if (rawValue == null) {
-          continue;
-        }
-        if (_config.isIgnoredKey(key)) {
-          _ignoredKeyDropCount++;
-          continue;
-        }
-        FieldSpec keySpec = _childFieldSpecs.get(key);
-        DataType valueType;
-        if (keySpec != null) {
-          valueType = keySpec.getDataType();
+      OpenStructKeyFlattener.flatten(map, _maxNestedKeyDepth, this::addEntry);
+    }
+    _numDocs++;
+  }
+
+  /// Accumulates one flat key of the current document. `container` marks a 
key whose value is a nested object
+  /// rendered as JSON text; see [#classify()] for why those are held out of 
automatic dense selection.
+  private void addEntry(String key, @Nullable Object rawValue, boolean 
container) {
+    if (rawValue == null) {
+      return;
+    }
+    if (_config.isIgnoredKey(key)) {
+      _ignoredKeyDropCount++;
+      return;
+    }
+    if (container) {
+      _containerKeys.add(key);
+    }
+    FieldSpec keySpec = _childFieldSpecs.get(key);
+    // Shape is decided by the first value the key presents and then sticks, 
exactly as its type does. A
+    // collection arriving on a key whose shape is already scalar is handled 
as any other value it cannot
+    // represent -- stringified on a STRING key, a coercion failure on a typed 
one -- rather than reshaping a
+    // column other documents already wrote to.
+    Object[] elements = OpenStructTypeInference.asMultiValue(rawValue);

Review Comment:
   :nit: 
   asMultiValue runs for every key of every row and the result is discarded ten 
lines later when the key is already single-value. Moving it behind that check 
costs nothing, though the saving is only two instanceof calls.



##########
pinot-spi/src/main/java/org/apache/pinot/spi/data/OpenStructKeyFlattener.java:
##########
@@ -0,0 +1,124 @@
+/**
+ * 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.spi.data;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.spi.utils.JsonUtils;
+
+
+/// Turns a nested OPEN_STRUCT document into flat keys, so a value buried 
under an object is
+/// addressable as a key of its own.
+///
+/// OPEN_STRUCT keys a document one level deep: `col['device']` names an entry 
of the top-level
+/// map. A document like `{"device": {"os": "ios"}}` therefore has exactly one 
key, `device`, whose
+/// value is an object -- there is no key that names the `os` inside it, so no 
column is ever
+/// materialized for it and no predicate can reach it without decoding the 
object per row.
+///
+/// Flattening makes the **path** the key: `device.os`. That key is 
materialized, indexed, filtered
+/// and projected like any other, with no change to the key/value contract -- 
`.` is an ordinary
+/// character in a key, so `col['device.os']` is already valid syntax.
+///
+/// The container keeps its own entry, serialized as JSON text, so 
`col['device']` still returns the
+/// whole object after its leaves have been split out.
+///
+/// ```
+/// {"device": {"os": "ios", "ver": 17}}   maxDepth = 2

Review Comment:
   What should happen when flattening produces a key the document already has?
   
   `.` is an ordinary key character.  That's what makes `col['device.os']` 
valid — and it's also the path separator, so at `maxNestedKeyDepth > 1` one 
document can emit the same key twice:
   
   ```json
   {"a.b": 100, "a": {"b": 200}}
   ```
   
   `flatten` emits `[a.b, a, a.b]` here. Should `a.b` be the literal key, the 
flattened path, last-one-wins — or is this document outside the contract?
   
   The two tiers answer differently today. Three docs at depth 2 
(`{"a.b":100,"a":{"b":200}}`, `{"a.b":300}`, `{"a.b":400}`):
   
   | tier | doc0 | doc1 | doc2 |
   |---|---|---|---|
   | consuming | 200 | 300 | 400 |
   | sealed | 100 | 200 | 300 |
   
   `MutableOpenStructIndex` writes by docId, so the second emission overwrites. 
`OpenStructColumnSplitter` appends to `_values` while `bitmap.add(_numDocs)` is 
idempotent, so the list runs one longer than the bitmap's cardinality — and 
`writeColumnIndexes` pairs them positionally (`values.get(ordinal++)`), 
shifting every later document. doc2's `400` is never written; no exception.
   
   Only reachable with the feature on, but dotted attribute names alongside 
nested objects is common in OTel-style data. 
`OpenStructConsumingSealedParityTest` has a nested-path case but no colliding 
one, so it passes.
   



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java:
##########
@@ -375,14 +477,20 @@ private void writeDenseKeyColumn(String key)
     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);
-    Object defaultValue = childFieldSpec.getDefaultNullValue();
+    boolean singleValue = !_multiValueKeys.contains(key);

Review Comment:
   This is the only read of `_multiValueKeys`, so the shape fix applies to the 
dense tier only. Which tier a key lands on is a tuning decision, not a property 
of the data.
   
   `writeSparseJsonColumn` never consults it, nothing forces a multi-value key 
dense, and an undeclared sparse key resolves through `getValueFieldSpec` to 
`new DimensionFieldSpec(key, STRING, true)`, served by a reader whose 
`isSingleValue()` is a literal `true` with no MV getters.
   
   Two segments, identical rows (`{"tags": ["a","b"]}` × 4), differing only in 
`maxDenseKeys`:
   
   ```
   DENSE   materialized=true   type=STRING  singleValue=false  doc0=[a, b]
   SPARSE  materialized=false  type=STRING  singleValue=true   doc0=["a","b"]
   ```
   
   One reports `STRING[]` with two elements, the other a scalar `STRING` 
holding `["a","b"]`. A query fans out over both, so the broker sees two shapes 
for one column and `col['tags'] = 'a'` matches only the first. A key drifting 
below `denseKeyMinFillRate` flips shape with no ingestion-time signal.
   
   Is dense-only deliberate for now? The commit reads as though the sparse blob 
covers the case, which is true of the blob but not of the reader over it.
   
   Could `OpenStructMultiValueKeyTest` get a sparse case? The ten new cases are 
all dense, which is why this passes. Same rows at `maxDenseKeys = -1` vs `0`, 
asserting the same shape and the same elements both ways, would pin it.
   



-- 
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