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


##########
pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java:
##########
@@ -0,0 +1,439 @@
+/**
+ * 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.common.function;
+
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Shared Fory JSON parser for opt-in JSON-path implementations.
+///
+/// Streaming extraction uses one single-state parser per worker thread to 
avoid shared-pool contention. Initialization
+/// or runtime linkage failures permanently disable the optional path, 
allowing callers to fall back to
+/// Jackson/Jayway. Jackson's default scalar token limits are enforced while 
walking the document, and inputs outside
+/// Fory's safe nesting depth fall back to the reference parser.
+public final class ForyJsonPathExtractor {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(ForyJsonPathExtractor.class);
+  private static final StreamReadConstraints JACKSON_CONSTRAINTS = 
StreamReadConstraints.defaults();
+  private static final ThreadLocal<PathContext> PATH_CONTEXT = 
ThreadLocal.withInitial(PathContext::new);
+  private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new 
AtomicBoolean();
+
+  private ForyJsonPathExtractor() {
+  }
+
+  private static final class Holder {
+    private static volatile boolean _streamingAvailable;
+    private static final ThreadLocal<ForyJson> STREAMING_PARSER = 
ThreadLocal.withInitial(() -> {
+      if (!_streamingAvailable) {
+        throw new IllegalStateException("Fory JSON is unavailable");
+      }
+      ForyJson parser = buildStreamingParser();
+      if (parser == null) {
+        _streamingAvailable = false;
+        throw new IllegalStateException("Fory JSON is unavailable");
+      }
+      return parser;
+    });
+
+    static {
+      ForyJson parser = buildStreamingParser();
+      _streamingAvailable = parser != null;
+      if (parser != null) {
+        STREAMING_PARSER.set(parser);
+      }
+    }
+
+    private Holder() {
+    }
+
+    @Nullable
+    private static ForyJson buildStreamingParser() {
+      try {
+        return 
ForyJson.builder().withCodegen(false).withAsyncCompilation(false).withConcurrencyLevel(1)
+            .registerCodec(PathResult.class, PathCodec.INSTANCE).build();
+      } catch (RuntimeException | LinkageError e) {
+        logUnavailable(e);
+        return null;
+      }
+    }
+  }
+
+  /// Returns whether the optional Fory runtime initialized successfully.
+  public static boolean isAvailable() {
+    return Holder._streamingAvailable;
+  }
+
+  /// Extracts a simple path with Fory's streaming reader without 
materializing the complete JSON tree.
+  ///
+  /// Unrelated values are still fully consumed so malformed input and 
duplicate-key last-wins behavior match the
+  /// reference parser. Jackson's nesting, field-name, string, and number 
limits are checked while scanning. Callers
+  /// should retry with the reference parser when this method throws.
+  @Nullable
+  public static Object extract(String json, SimpleJsonPath path) {
+    if ((JACKSON_CONSTRAINTS.hasMaxDocumentLength()
+        && json.length() > JACKSON_CONSTRAINTS.getMaxDocumentLength())
+        || (JACKSON_CONSTRAINTS.hasMaxTokenCount() && 
requiresJacksonFallback(json))) {
+      throw new IllegalArgumentException("JSON document requires Jackson 
constraint validation");
+    }

Review Comment:
   Fixed in f0f1036c99. The dead lexical prescan is removed. Document length is 
checked up front; nesting, field-name, string, and number limits are enforced 
during the streaming walk; configured maxTokenCount is counted in that same 
walk. The focused configured-token-limit child-JVM test passes.



##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -129,6 +130,25 @@ private static Object fastJsonPath(Object object, String 
jsonPath, boolean useBi
     return jsonPath(object, jsonPath);
   }
 
+  /// Resolves a simple path over a JSON string with Fory's streaming reader. 
Complex paths, already-parsed inputs,
+  /// container results, and Fory extraction failures use the existing Jayway 
implementation so the experimental
+  /// functions below retain the current behavior outside Fory's supported 
envelope.
+  @Nullable
+  private static Object foryJsonPath(Object object, String jsonPath) {
+    SimpleJsonPath simpleJsonPath = SimpleJsonPath.compile(jsonPath);
+    if (!(object instanceof String) || simpleJsonPath == null) {
+      return jsonPath(object, jsonPath);
+    }

Review Comment:
   Fixed in f0f1036c99. foryJsonPath now checks object instanceof String before 
compiling SimpleJsonPath, so Map/List and other non-String inputs go directly 
to the existing Jayway path without the extra compile.



##########
pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java:
##########
@@ -0,0 +1,218 @@
+/**
+ * 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.perf;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.LiteralContext;
+import org.apache.pinot.core.common.BlockValSet;
+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.core.operator.transform.function.BaseTransformFunction;
+import 
org.apache.pinot.core.operator.transform.function.JsonExtractScalarTransformFunction;
+import 
org.apache.pinot.core.operator.transform.function.LiteralTransformFunction;
+import org.apache.pinot.core.operator.transform.function.TransformFunction;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OperationsPerInvocation;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+
+/// Measures the actual query-side `jsonExtractScalar*` ValueBlock loop. Each 
invocation processes a 128-row block;
+/// JMH normalizes throughput and allocation to one row via 
[OperationsPerInvocation]. Input projection is represented
+/// by a pre-materialized String array so the comparison isolates JSON 
extraction, type coercion, and result writing.
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 3)
+@State(Scope.Thread)
+public class BenchmarkJsonExtractScalarQuery {
+  private static final int BLOCK_ROWS = 128;
+  private static final TransformResultMetadata STRING_METADATA =
+      new TransformResultMetadata(DataType.STRING, true, false);
+  private static final String BASE_JSON = "{"
+      + "\"earlyMetric\":17,"
+      + 
"\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"tier\":\"gold\",\"age\":41},"
+      + 
"\"event\":{\"name\":\"checkout\",\"cart\":[{\"sku\":\"A1\",\"qty\":2,\"price\":19.99},"
+      + 
"{\"sku\":\"B7\",\"qty\":1,\"price\":129.0},{\"sku\":\"C3\",\"qty\":4,\"price\":3.5}],"
+      + "\"currency\":\"USD\",\"coupon\":null},"
+      + 
"\"device\":{\"os\":\"macOS\",\"version\":\"14.5\",\"browser\":\"Chrome\","
+      + "\"screen\":{\"width\":1728,\"height\":1117}},"
+      + "\"geo\":{\"city\":\"San 
Francisco\",\"region\":\"CA\",\"country\":\"US\","
+      + "\"coordinates\":[-122.4194,37.7749]},"
+      + 
"\"attributes\":{\"campaign\":\"summer-sale\",\"referrer\":\"search\",\"experiment\":\"checkout-v2\"},"
+      + "\"flags\":[\"returning\",\"subscribed\",\"beta\"],"
+      + "\"lateMetric\":19} ";
+
+  @Param({"early", "late"})
+  private String _fieldPosition;
+
+  @Param({"700", "8192", "65536"})
+  private int _documentBytes;
+
+  private ValueBlock _valueBlock;
+  private JsonExtractScalarTransformFunction _jayway;
+  private JsonExtractScalarTransformFunction _fast;
+  private JsonExtractScalarTransformFunction _firstMatch;
+  private JsonExtractScalarTransformFunction _fory;
+
+  @Setup
+  public void setUp() {
+    String json = buildJson(_documentBytes);
+    String[] jsonRows = new String[BLOCK_ROWS];
+    Arrays.fill(jsonRows, json);
+    TransformFunction input = new StringArrayTransformFunction(jsonRows);
+    String path = "early".equals(_fieldPosition) ? "$.earlyMetric" : 
"$.lateMetric";
+    List<TransformFunction> arguments = List.of(input, literal(path), 
literal("LONG"));
+
+    _valueBlock = new FixedValueBlock(BLOCK_ROWS);
+    _jayway = new JsonExtractScalarTransformFunction();
+    _fast = new JsonExtractScalarTransformFunction.Fast();
+    _firstMatch = new JsonExtractScalarTransformFunction.FirstMatch();
+    _fory = new JsonExtractScalarTransformFunction.Fory();
+    for (JsonExtractScalarTransformFunction function : List.of(_jayway, _fast, 
_firstMatch, _fory)) {
+      function.init(arguments, Map.<String, ColumnContext>of(), false);
+      long expected = "early".equals(_fieldPosition) ? 17L : 19L;
+      long[] values = function.transformToLongValuesSV(_valueBlock);
+      if (values.length < BLOCK_ROWS || values[0] != expected || 
values[BLOCK_ROWS - 1] != expected) {
+        throw new IllegalStateException(function.getName() + " produced an 
unexpected query result");
+      }
+    }
+  }
+
+  private static LiteralTransformFunction literal(String value) {
+    return new LiteralTransformFunction(new LiteralContext(DataType.STRING, 
value));
+  }
+
+  private static String buildJson(int targetBytes) {
+    if (BASE_JSON.length() >= targetBytes) {
+      return BASE_JSON;
+    }
+    String marker = "\"lateMetric\":";
+    int markerOffset = BASE_JSON.indexOf(marker);
+    String paddingPrefix = "\"padding\":\"";
+    String paddingSuffix = "\",";
+    int paddingLength = targetBytes - BASE_JSON.length() - 
paddingPrefix.length() - paddingSuffix.length();
+    return BASE_JSON.substring(0, markerOffset) + paddingPrefix + 
"x".repeat(paddingLength) + paddingSuffix
+        + BASE_JSON.substring(markerOffset);

Review Comment:
   Fixed in f0f1036c99. buildJson now returns the base document when the 
prefix/suffix leave no positive padding length, avoiding a negative 
String.repeat argument.



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java:
##########
@@ -72,6 +74,10 @@
 /// keys to the first non-null occurrence and does not validate malformed 
content after the resolved value. Use it
 /// only for well-formed, duplicate-free JSON. `Fast` scans the full root 
value and retains Jayway's last-key-wins
 /// and malformed-document behavior; see [FastJsonPathExtractor] for one 
documented unaddressed-value edge case.
+/// `jsonExtractScalarFory` is experimental and must be selected explicitly. 
It accelerates simple paths over
+/// `STRING` input for scalar result types other than `STRING` and 
`BIG_DECIMAL`. `BYTES` input, complex paths,
+/// containers / array result types, precision-sensitive results, deeply 
nested documents, and Fory failures use
+/// Jayway. Its name, supported envelope, and implementation can change while 
the integration is evaluated.

Review Comment:
   Resolved in f0f1036c99 with explicit initialization-time eligibility. Fory 
is eligible only for simple paths, STRING input, single-value output, and 
result types other than STRING/JSON/BIG_DECIMAL. The Javadoc now states this 
exact envelope, and the eligibility matrix test covers it.



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java:
##########
@@ -802,7 +817,7 @@ private <T> IntFunction<T> getResultExtractor(ValueBlock 
valueBlock, ParseContex
     if (_jsonFieldTransformFunction.getResultMetadata().getDataType() == 
DataType.BYTES) {
       byte[][] jsonBytes = 
_jsonFieldTransformFunction.transformToBytesValuesSV(valueBlock);
       IntFunction<T> jaywayExtractor = i -> 
parseContext.parseUtf8(jsonBytes[i]).read(_jsonPath);
-      if (_simpleJsonPath == null) {
+      if (_simpleJsonPath == null || useBigDecimal || _extractionMode == 
ExtractionMode.FORY) {

Review Comment:
   Fixed in f0f1036c99. The BYTES branch no longer gates Fast/FirstMatch on 
useBigDecimal; only Fory uses Jayway for BYTES. I added a non-vacuous 
regression using a precise addressed decimal plus a hostile unaddressed 
exponent that Jayway rejects but Fast/FirstMatch skip. The 246-case focused 
suite passes.



##########
pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java:
##########
@@ -0,0 +1,439 @@
+/**
+ * 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.common.function;
+
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Shared Fory JSON parser for opt-in JSON-path implementations.
+///
+/// Streaming extraction uses one single-state parser per worker thread to 
avoid shared-pool contention. Initialization
+/// or runtime linkage failures permanently disable the optional path, 
allowing callers to fall back to
+/// Jackson/Jayway. Jackson's default scalar token limits are enforced while 
walking the document, and inputs outside
+/// Fory's safe nesting depth fall back to the reference parser.
+public final class ForyJsonPathExtractor {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(ForyJsonPathExtractor.class);
+  private static final StreamReadConstraints JACKSON_CONSTRAINTS = 
StreamReadConstraints.defaults();
+  private static final ThreadLocal<PathContext> PATH_CONTEXT = 
ThreadLocal.withInitial(PathContext::new);
+  private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new 
AtomicBoolean();
+
+  private ForyJsonPathExtractor() {
+  }
+
+  private static final class Holder {
+    private static volatile boolean _streamingAvailable;
+    private static final ThreadLocal<ForyJson> STREAMING_PARSER = 
ThreadLocal.withInitial(() -> {
+      if (!_streamingAvailable) {
+        throw new IllegalStateException("Fory JSON is unavailable");
+      }
+      ForyJson parser = buildStreamingParser();
+      if (parser == null) {
+        _streamingAvailable = false;
+        throw new IllegalStateException("Fory JSON is unavailable");
+      }
+      return parser;
+    });
+
+    static {
+      ForyJson parser = buildStreamingParser();
+      _streamingAvailable = parser != null;
+      if (parser != null) {
+        STREAMING_PARSER.set(parser);
+      }
+    }
+
+    private Holder() {
+    }
+
+    @Nullable
+    private static ForyJson buildStreamingParser() {
+      try {
+        return 
ForyJson.builder().withCodegen(false).withAsyncCompilation(false).withConcurrencyLevel(1)
+            .registerCodec(PathResult.class, PathCodec.INSTANCE).build();
+      } catch (RuntimeException | LinkageError e) {
+        logUnavailable(e);
+        return null;
+      }
+    }
+  }
+
+  /// Returns whether the optional Fory runtime initialized successfully.
+  public static boolean isAvailable() {
+    return Holder._streamingAvailable;
+  }
+
+  /// Extracts a simple path with Fory's streaming reader without 
materializing the complete JSON tree.
+  ///
+  /// Unrelated values are still fully consumed so malformed input and 
duplicate-key last-wins behavior match the
+  /// reference parser. Jackson's nesting, field-name, string, and number 
limits are checked while scanning. Callers
+  /// should retry with the reference parser when this method throws.
+  @Nullable
+  public static Object extract(String json, SimpleJsonPath path) {
+    if ((JACKSON_CONSTRAINTS.hasMaxDocumentLength()
+        && json.length() > JACKSON_CONSTRAINTS.getMaxDocumentLength())
+        || (JACKSON_CONSTRAINTS.hasMaxTokenCount() && 
requiresJacksonFallback(json))) {
+      throw new IllegalArgumentException("JSON document requires Jackson 
constraint validation");
+    }
+    if (!Holder._streamingAvailable) {
+      throw new IllegalStateException("Fory JSON is unavailable");
+    }
+    ForyJson parser = Holder.STREAMING_PARSER.get();
+    PathContext context = PATH_CONTEXT.get();
+    if (context._active) {
+      throw new IllegalStateException("Fory JSON path extraction is not 
reentrant");
+    }
+    context._active = true;
+    context._path = path;
+    context._result = null;
+    try {
+      parser.fromJson(json, PathResult.class);
+      return context._result;
+    } catch (LinkageError e) {
+      disable();
+      logUnavailable(e);
+      throw new IllegalStateException("Fory JSON became unavailable", e);
+    } finally {
+      context._path = null;
+      context._result = null;
+      context._active = false;
+    }
+  }
+
+  private static void disable() {
+    Holder._streamingAvailable = false;
+    Holder.STREAMING_PARSER.remove();
+    PATH_CONTEXT.remove();
+  }
+
+  private static void logUnavailable(Throwable cause) {
+    if (UNAVAILABLE_WARNING_LOGGED.compareAndSet(false, true)) {
+      LOGGER.warn("Experimental Fory JSON support is unavailable; falling back 
to Jackson/Jayway", cause);
+    }
+  }
+
+  private static Object readPath(JsonReader reader, SimpleJsonPath path, int 
depth) {
+    String key = path.getKey(depth);
+    if (key != null) {
+      return readObjectPath(reader, path, depth, key);
+    }
+    return readArrayPath(reader, path, depth, path.getIndex(depth));
+  }
+
+  @Nullable
+  private static Object readObjectPath(JsonReader reader, SimpleJsonPath path, 
int depth, String expectedKey) {
+    if (reader.peekToken() != '{') {
+      skipValue(reader);
+      return null;
+    }
+    reader.enterDepth();
+    try {
+      reader.expect('{');
+      if (reader.consume('}')) {
+        return null;
+      }
+      Object result = null;
+      boolean more;
+      do {
+        String fieldName = reader.readFieldName();
+        if (fieldName.length() > JACKSON_CONSTRAINTS.getMaxNameLength()) {
+          throw new IllegalArgumentException("JSON field name exceeds 
Jackson's configured limit");
+        }
+        reader.expect(':');
+        if (expectedKey.equals(fieldName)) {
+          result = depth + 1 == path.length() ? readScalar(reader) : 
readPath(reader, path, depth + 1);
+        } else {
+          skipValue(reader);
+        }
+        more = reader.consumeCommaOrEndObject();
+      } while (more);
+      return result;
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  @Nullable
+  private static Object readArrayPath(JsonReader reader, SimpleJsonPath path, 
int depth, int expectedIndex) {
+    if (reader.peekToken() != '[') {
+      skipValue(reader);
+      return null;
+    }
+    reader.enterDepth();
+    try {
+      reader.expect('[');
+      if (reader.consume(']')) {
+        return null;
+      }
+      Object result = null;
+      int index = 0;
+      boolean more;
+      do {
+        if (index == expectedIndex) {
+          result = depth + 1 == path.length() ? readScalar(reader) : 
readPath(reader, path, depth + 1);
+        } else {
+          skipValue(reader);
+        }
+        index++;
+        more = reader.consumeCommaOrEndArray();
+      } while (more);
+      return result;
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  @Nullable
+  private static Object readScalar(JsonReader reader) {
+    char token = reader.peekToken();
+    if (token == '"') {
+      String value = reader.readString();
+      if (value.length() > JACKSON_CONSTRAINTS.getMaxStringLength()) {
+        throw new IllegalArgumentException("JSON string exceeds Jackson's 
configured limit");
+      }
+      return value;
+    }
+    if (token == 't' || token == 'f') {
+      return reader.readBoolean();
+    }
+    if (token == 'n') {
+      reader.readNull();
+      return null;
+    }
+    if (token == '{' || token == '[') {
+      // Query scalar coercion has observable error/default behavior for 
containers. Let Jayway produce the exact
+      // reference value rather than materializing a Fory container on this 
uncommon path.
+      throw new IllegalArgumentException("Container result requires reference 
JSON parsing");

Review Comment:
   Fixed in f0f1036c99. Container leaves are fully consumed and return a 
private identity sentinel instead of throwing per row; callers then use Jayway 
for exact container/coercion behavior. Duplicate container/scalar order and 
malformed-tail tests cover the sentinel path. At T32, object/array Fory 
allocation dropped from about 2.9/2.8 KiB to 2.2/1.8 KiB per op.



##########
pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java:
##########
@@ -0,0 +1,439 @@
+/**
+ * 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.common.function;
+
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Shared Fory JSON parser for opt-in JSON-path implementations.
+///
+/// Streaming extraction uses one single-state parser per worker thread to 
avoid shared-pool contention. Initialization
+/// or runtime linkage failures permanently disable the optional path, 
allowing callers to fall back to
+/// Jackson/Jayway. Jackson's default scalar token limits are enforced while 
walking the document, and inputs outside
+/// Fory's safe nesting depth fall back to the reference parser.
+public final class ForyJsonPathExtractor {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(ForyJsonPathExtractor.class);
+  private static final StreamReadConstraints JACKSON_CONSTRAINTS = 
StreamReadConstraints.defaults();
+  private static final ThreadLocal<PathContext> PATH_CONTEXT = 
ThreadLocal.withInitial(PathContext::new);
+  private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new 
AtomicBoolean();
+
+  private ForyJsonPathExtractor() {
+  }
+
+  private static final class Holder {
+    private static volatile boolean _streamingAvailable;
+    private static final ThreadLocal<ForyJson> STREAMING_PARSER = 
ThreadLocal.withInitial(() -> {

Review Comment:
   Fixed in f0f1036c99. The per-caller ThreadLocal<ForyJson> is replaced by one 
process-wide, thread-safe Fory facade with Fory's fixed default state pool; 
only the small path/result context remains thread-local and is cleared after 
each call. T32 ValueBlock Fory remains 1.15-1.32x Fast at 700 B and 3.62-4.13x 
at 64 KiB. The pool is bounded, though steady-state JMH does not measure its 
retained heap.



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