xiangfu0 commented on code in PR #19481:
URL: https://github.com/apache/pinot/pull/19481#discussion_r4091529166


##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/metadata/SegmentMetadataImpl.java:
##########
@@ -535,17 +621,113 @@ public String getEndOffset() {
     return _endOffset;
   }
 
+  /// {@inheritDoc}
+  ///
+  /// Built from the column arrays on the first call and cached until the 
columns change, so a caller pays one map
+  /// entry per column and the segment keeps it for its lifetime. Nothing on 
the load or query path should call this
+  /// — see the accessors listed on [SegmentMetadata#getColumnMetadataMap()]. 
Writes to the returned map do not reach
+  /// the segment metadata; use [#addColumnMetadata(String, ColumnMetadata)] 
and [#removeColumn(String)] instead.
+  ///
+  /// Returns `null` for a CONSUMING segment, which holds no column metadata.
+  @Nullable
   @Override
   public TreeMap<String, ColumnMetadata> getColumnMetadataMap() {

Review Comment:
   **MAJOR [C1.3 / C5.22]:** before, this returned the live backing `TreeMap` 
and every other accessor derived from it, so both a `put` into the map and a 
subclass override were honoured everywhere. Now the arrays are authoritative 
and this is a lazily built, cached, plain mutable `TreeMap`. Two consequences:
   
   1. A caller writing into the returned map (the pattern 
`ImmutableSegmentLoader:334` itself used at base) gets a silently inconsistent 
object: the write shows through the cached map but not through 
`getColumnMetadataFor` / `getAllColumns` / `getSchema` / `toJson`. Nothing 
fails.
   2. Subclass overrides are bypassed. startree-pinot's 
`DedupSnapshotCreationTaskUtils` and `UpsertSnapshotCreationTaskUtils` override 
`getColumnMetadataMap()` to return an empty `TreeMap` so the eager 
`ImmutableSegmentImpl` constructor sees no columns; the constructor now reads 
`getAllColumns()` / `getNumColumns()` / `forEachColumn()`, so the override is 
dead (details in the review body).
   
   Fix: make the derived map fail loudly on writes (an anonymous `TreeMap` 
subclass throwing from `put` / `putAll` / `remove` / `clear` / `putIfAbsent` / 
`compute*` / `merge` / `replace*` / `pollFirstEntry` / `pollLastEntry`; 
iterator removal cannot be intercepted, so keep the Javadoc), or return a fresh 
copy per call; state in the SPI Javadoc and the PR description that 
`getColumnMetadataMap()` is a derived view and overrides no longer feed other 
accessors; and file the startree-pinot follow-up.



##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/SegmentMetadata.java:
##########
@@ -113,13 +125,53 @@ default NavigableSet<String> getAllColumns() {
     return getSchema().getColumnNames();
   }
 
+  /// Number of columns in [#getAllColumns()].
+  ///
+  /// A segment that holds no column metadata (a CONSUMING one, built from an 
explicit schema) still reports its
+  /// schema's columns here, so this is not the size of 
[#getAllColumnMetadata()]: do not pair the two.
+  default int getNumColumns() {
+    return getColumnMetadataMap().size();

Review Comment:
   **MINOR:** the new defaults `getNumColumns()`, `getAllColumnMetadata()` and 
`forEachColumn()` NPE when `getColumnMetadataMap()` returns `null`, while their 
Javadoc promises schema columns / empty. 
`PartitionIdVirtualColumnProvider.java:89-91` also dropped the base null guard 
when it switched to `forEachColumn`, so an external implementer returning 
`null` for consuming segments (the pre-PR convention) would NPE there. None 
exists today in pinot or startree-pinot `src/main`. Make the defaults 
null-tolerant (#19486 does it for `getAllColumnMetadata` only; better to do all 
of them here).



##########
pinot-server/src/main/java/org/apache/pinot/server/api/resources/TablesResource.java:
##########
@@ -252,15 +252,17 @@ public String getSegmentMetadata(
 
             Set<String> allSegmentColumns = segmentMetadata.getAllColumns();
             if (columnSet == null) {
-              columnSet = allSegmentColumns;
+              // Copy: getAllColumns() is an unmodifiable view of the 
segment's own columns, and retainAll below
+              // would otherwise narrow the first segment's metadata rather 
than the running intersection.
+              columnSet = new HashSet<>(allSegmentColumns);

Review Comment:
   **MAJOR [PROC] — this is a real fix, and it should land first.** With 
`columns=*` this endpoint captured the first immutable segment's 
`getAllColumns()` and then `retainAll`'d every later segment into it, which 
removes through the iterator, and the set was live:
   
   - master / stack base: `SegmentMetadata.getAllColumns()` defaults to 
`getSchema().getColumnNames()` = `Schema._fieldSpecMap.navigableKeySet()`. The 
endpoint deletes FieldSpecs from the first segment's own `Schema` for every 
column a later segment lacks (the normal case after schema evolution). 
`ImmutableSegmentImpl.getColumnNames()` / `getPhysicalColumnNames()` read that 
schema live and `SelectionOperatorUtils:91` expands `SELECT *` from it, so that 
segment silently returns fewer columns until reload. Unchanged on 
upstream/master today; the controller forwards the caller's `columns` list 
verbatim, so `GET /tables/{table}/metadata?columns=*` on the controller 
triggers it.
   - #19478 / #19479 heads: `getAllColumns()` returns 
`_columnMetadataMap.navigableKeySet()`, so the same call deletes 
`ColumnMetadata` from a serving segment.
   - here: fixed by the copy, and `getAllColumns()` is now an unmodifiable 
snapshot (would throw rather than corrupt), pinned by 
`SegmentMetadataImplTest.testGetAllColumnsIsAnUnmodifiableSnapshot`.
   
   Please lift this copy into a standalone hotfix PR against master with a 
regression test (two segments with different column sets, call the endpoint 
with `*`, assert the first segment's `getColumnNames()` is unchanged), merge 
it, and rebase the stack on it — then no PR in the stack carries the window. 
Also call the user-visible fix out in this PR's description.



##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/metadata/SegmentMetadataImpl.java:
##########
@@ -436,11 +473,60 @@ public boolean isSchemaMaterialized() {
     return _schema != null;
   }
 
-  /// The keys of the column metadata map, i.e. the same names as 
`getSchema().getColumnNames()` without building the
-  /// schema. Falls back to the explicit schema of a CONSUMING segment, which 
has no column metadata map.
+  /// Number of schemas derived from column metadata so far in this JVM. A 
load or query path that leaves this
+  /// unchanged did not build any segment's schema.
+  @VisibleForTesting
+  public static long getNumSchemaMaterializations() {
+    return NUM_SCHEMA_MATERIALIZATIONS.get();
+  }
+
+  /// An unmodifiable view of the sorted column name array, i.e. the same 
names as `getSchema().getColumnNames()`
+  /// without building the schema. Falls back to the explicit schema of a 
CONSUMING segment, which has no column
+  /// metadata. The view is a snapshot: it does not reflect columns added or 
removed after this call.
   @Override
   public NavigableSet<String> getAllColumns() {

Review Comment:
   **MINOR (pre-existing):** the Javadoc says "unmodifiable view … falls back 
to the explicit schema", but the CONSUMING fallback 
`getSchema().getColumnNames()` is the live, mutable `navigableKeySet()` of the 
shared table `Schema` for `MutableSegmentImpl`. Wrap it in 
`Collections.unmodifiableNavigableSet(...)` so both branches honour the 
contract.



##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/metadata/SortedStringArraySet.java:
##########
@@ -0,0 +1,313 @@
+/**
+ * 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.spi.index.metadata;
+
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.NavigableSet;
+import java.util.NoSuchElementException;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/// Unmodifiable [NavigableSet] view of a range of a sorted, duplicate-free 
`String[]`, ordered naturally.
+///
+/// Lookups are a binary search over the array, so the whole set costs one 
small object rather than a red-black-tree
+/// node per element. That is the point: a server holds the column names of 
every loaded segment for the segment's
+/// lifetime, and a wide segment has thousands of them.
+///
+/// The array is referenced, not copied, so the view reflects nothing the 
holder does afterwards *except* in-place
+/// writes: [SegmentMetadataImpl] replaces its arrays when its columns change, 
which leaves an already-returned view
+/// as the snapshot taken at the time of the call.
+///
+/// `subSet`/`headSet`/`tailSet` are ranges of the same array, and like every 
[NavigableSet] range view they reject
+/// an argument outside their own range rather than silently widening it.
+///
+/// Immutable and thread-safe as long as the backing array is not written in 
place.
+final class SortedStringArraySet extends AbstractSet<String> implements 
NavigableSet<String> {
+  private final String[] _elements;
+  private final int _from;
+  private final int _to;
+  /// The bounds this view was created with, `null` on the side it is 
unbounded on. A range view has to reject an
+  /// argument outside its own range, as [NavigableSet] requires, which the 
array indices alone cannot tell: an
+  /// exclusive endpoint that is not in the array leaves no trace in them.
+  @Nullable
+  private final String _low;
+  private final boolean _lowInclusive;
+  @Nullable
+  private final String _high;
+  private final boolean _highInclusive;
+
+  SortedStringArraySet(String[] elements) {
+    this(elements, 0, elements.length, null, false, null, false);
+  }
+
+  private SortedStringArraySet(String[] elements, int from, int to, @Nullable 
String low, boolean lowInclusive,
+      @Nullable String high, boolean highInclusive) {
+    _elements = elements;
+    _from = from;
+    _to = to;
+    _low = low;
+    _lowInclusive = lowInclusive;
+    _high = high;
+    _highInclusive = highInclusive;
+  }
+
+  @Override
+  public int size() {
+    return _to - _from;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return _from == _to;
+  }
+
+  @Override
+  public boolean contains(Object o) {

Review Comment:
   **MINOR (doc):** `contains(null)` returns `false` here where 
`TreeSet.contains(null)` throws `NullPointerException`. Benign (no caller 
passes null), but note the difference in the class Javadoc since the class is 
documented as a `TreeSet` stand-in.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java:
##########
@@ -253,21 +246,14 @@ public ImmutableSegmentImpl(
   /// metadata declares them complex (the same rule the eager constructor 
applies).
   @Nullable
   private static Map<String, List<String>> 
groupOpenStructChildren(SegmentMetadataImpl segmentMetadata) {
-    Map<String, List<String>> children = null;
-    Map<String, ColumnMetadata> columnMetadataMap = 
segmentMetadata.getColumnMetadataMap();
-    for (Map.Entry<String, ColumnMetadata> entry : 
columnMetadataMap.entrySet()) {
-      if (entry.getValue() instanceof ColumnMetadataImpl impl && 
impl.isMaterializedChild()) {
-        if (children == null) {
-          children = new HashMap<>();
-        }
-        children.computeIfAbsent(impl.getParentColumn(), k -> new 
ArrayList<>()).add(entry.getKey());
+    Map<String, List<String>> children = new HashMap<>();
+    segmentMetadata.forEachColumn((column, columnMetadata) -> {
+      if (columnMetadata instanceof ColumnMetadataImpl impl && 
impl.isMaterializedChild()) {
+        children.computeIfAbsent(impl.getParentColumn(), k -> new 
ArrayList<>()).add(column);

Review Comment:
   **MINOR (nit):** `groupOpenStructChildren` now always allocates the 
`HashMap` (base allocated only when an open-struct child existed). Result 
identical, load-time only; fine to leave.



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