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

xiangfu0 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 cb89c0e4c56 Add experimental Fory JSON functions (#19223)
cb89c0e4c56 is described below

commit cb89c0e4c566005059d3b6b97ad306f2e159ae98
Author: Xiang Fu <[email protected]>
AuthorDate: Thu Aug 13 01:02:28 2026 -0700

    Add experimental Fory JSON functions (#19223)
    
    * Add experimental Fory JSON functions
    
    * Optimize experimental Fory JSON extraction
    
    * Benchmark Fory JSON function counterparts
    
    * Address Fory JSON review feedback
    
    * Clarify optional Fory runtime requirements
    
    ---------
    
    Co-authored-by: Xiang Fu <[email protected]>
---
 licenses-binary/LICENSE-kryo.txt                   |   2 +-
 pinot-common/pom.xml                               |  18 +
 .../common/function/ForyJsonPathExtractor.java     | 388 +++++++++++++++++++++
 .../common/function/TransformFunctionType.java     |   2 +
 .../common/function/scalar/JsonFunctions.java      |  75 ++++
 .../org/apache/pinot/sql/parsers/ParserUtils.java  |   3 +
 .../common/function/FastJsonPathExtractorTest.java |   1 +
 .../function/ForyJsonLinkageFallbackTest.java      | 143 ++++++++
 .../common/function/ForyJsonPathFunctionsTest.java | 298 ++++++++++++++++
 pinot-core/pom.xml                                 |   5 +
 .../JsonExtractScalarTransformFunction.java        |  54 ++-
 .../function/TransformFunctionFactory.java         |   2 +
 .../core/data/function/JsonFunctionsTest.java      |  23 ++
 .../pinot/core/function/FunctionRegistryTest.java  |   2 +-
 .../JsonExtractScalarTransformFunctionTest.java    |  53 ++-
 pinot-integration-tests/pom.xml                    |   5 +
 .../integration/tests/custom/JsonPathTest.java     |  47 ++-
 .../resources/udf-test-results/all-functions.yaml  |  16 +
 pinot-perf/pom.xml                                 |   4 +
 .../pinot/perf/BenchmarkForyJsonFallback.java      | 109 ++++++
 .../perf/BenchmarkJsonExtractScalarQuery.java      | 351 +++++++++++++++++++
 .../pinot/perf/BenchmarkJsonPathExtraction.java    | 241 +++++++++++--
 .../apache/pinot/query/QueryCompilationTest.java   |   6 +-
 .../pinot/query/QueryEnvironmentTestBase.java      |   2 +
 .../query/runtime/queries/QueryRunnerTest.java     |   4 +-
 pom.xml                                            |   6 +
 26 files changed, 1819 insertions(+), 41 deletions(-)

diff --git a/licenses-binary/LICENSE-kryo.txt b/licenses-binary/LICENSE-kryo.txt
index bf531009ca5..a64798f5c0b 100644
--- a/licenses-binary/LICENSE-kryo.txt
+++ b/licenses-binary/LICENSE-kryo.txt
@@ -7,4 +7,4 @@ Redistribution and use in source and binary forms, with or 
without modification,
 * Redistributions in binary form must reproduce the above copyright notice, 
this list of conditions and the following disclaimer in the documentation 
and/or other materials provided with the distribution.
 * Neither the name of Esoteric Software nor the names of its contributors may 
be used to endorse or promote products derived from this software without 
specific prior written permission.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROF [...]
\ No newline at end of file
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROF [...]
diff --git a/pinot-common/pom.xml b/pinot-common/pom.xml
index a36aea1d936..539625d7f2e 100644
--- a/pinot-common/pom.xml
+++ b/pinot-common/pom.xml
@@ -98,6 +98,19 @@
           </java>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <artifactSet>
+            <excludes>
+              <!-- Experimental Fory support is activated only when 
applications add Fory to their classpath. -->
+              <exclude>org.apache.fory:fory-json</exclude>
+              <exclude>org.apache.fory:fory-core</exclude>
+            </excludes>
+          </artifactSet>
+        </configuration>
+      </plugin>
       <!-- Stage 1 of SQL parser codegen: FMPP plugs Pinot's custom grammar 
(config.fmpp +
            templates/Parser.jj) into Calcite's parser template and writes the 
JavaCC grammar to
            target/generated-sources/javacc/Parser.jj. Declared before 
javacc-maven-plugin so it runs
@@ -280,6 +293,11 @@
       <groupId>com.jayway.jsonpath</groupId>
       <artifactId>json-path</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.fory</groupId>
+      <artifactId>fory-json</artifactId>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.apache.zookeeper</groupId>
       <artifactId>zookeeper</artifactId>
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java
new file mode 100644
index 00000000000..bfe32acc725
--- /dev/null
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java
@@ -0,0 +1,388 @@
+/**
+ * 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 bounded parser pool shared by all worker 
threads. Initialization or runtime linkage
+/// failures permanently disable the optional path, allowing callers to fall 
back to Jackson/Jayway. Jackson's
+/// document, token, nesting, field-name, string, and number constraints are 
enforced while walking the document.
+/// The Fory runtime is an optional dependency; applications must add 
`org.apache.fory:fory-json:1.6.0` (and its
+/// transitive `fory-core` dependency) to the application classpath to enable 
this experimental path.
+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 static final Object FALLBACK_REQUIRED = new Object();
+
+  private ForyJsonPathExtractor() {
+  }
+
+  private static final class Holder {
+    @Nullable
+    private static final ForyJson STREAMING_PARSER = buildStreamingParser();
+    private static volatile boolean _streamingAvailable = STREAMING_PARSER != 
null;
+
+    private Holder() {
+    }
+
+    @Nullable
+    private static ForyJson buildStreamingParser() {
+      try {
+        return 
ForyJson.builder().withCodegen(false).withAsyncCompilation(false)
+            .maxDepth(JACKSON_CONSTRAINTS.getMaxNestingDepth())
+            .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;
+  }
+
+  /// Returns whether an extracted value requires the reference parser for 
exact container/coercion semantics.
+  public static boolean isFallbackRequired(@Nullable Object value) {
+    return value == FALLBACK_REQUIRED;
+  }
+
+  /// 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 configured limits are checked while 
scanning. Callers should retry with the
+  /// reference parser when this method throws or 
[#isFallbackRequired(Object)] returns `true`.
+  @Nullable
+  public static Object extract(String json, SimpleJsonPath path) {
+    if (JACKSON_CONSTRAINTS.hasMaxDocumentLength()
+        && json.length() > JACKSON_CONSTRAINTS.getMaxDocumentLength()) {
+      throw new IllegalArgumentException("JSON document requires Jackson 
constraint validation");
+    }
+    if (!Holder._streamingAvailable) {
+      throw new IllegalStateException("Fory JSON is unavailable");
+    }
+    ForyJson parser = Holder.STREAMING_PARSER;
+    if (parser == null) {
+      throw new IllegalStateException("Fory JSON is unavailable");
+    }
+    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;
+    context._tokenCount = 0;
+    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._tokenCount = 0;
+      context._active = false;
+    }
+  }
+
+  private static void disable() {
+    Holder._streamingAvailable = false;
+    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, PathContext context) {
+    String key = path.getKey(depth);
+    if (key != null) {
+      return readObjectPath(reader, path, depth, key, context);
+    }
+    return readArrayPath(reader, path, depth, path.getIndex(depth), context);
+  }
+
+  @Nullable
+  private static Object readObjectPath(JsonReader reader, SimpleJsonPath path, 
int depth, String expectedKey,
+      PathContext context) {
+    if (reader.peekToken() != '{') {
+      skipValue(reader, context);
+      return null;
+    }
+    reader.enterDepth();
+    try {
+      countToken(context);
+      reader.expect('{');
+      if (reader.consume('}')) {
+        countToken(context);
+        return null;
+      }
+      Object result = null;
+      boolean more;
+      do {
+        countToken(context);
+        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, context)
+              : readPath(reader, path, depth + 1, context);
+        } else {
+          skipValue(reader, context);
+        }
+        more = reader.consumeCommaOrEndObject();
+      } while (more);
+      countToken(context);
+      return result;
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  @Nullable
+  private static Object readArrayPath(JsonReader reader, SimpleJsonPath path, 
int depth, int expectedIndex,
+      PathContext context) {
+    if (reader.peekToken() != '[') {
+      skipValue(reader, context);
+      return null;
+    }
+    reader.enterDepth();
+    try {
+      countToken(context);
+      reader.expect('[');
+      if (reader.consume(']')) {
+        countToken(context);
+        return null;
+      }
+      Object result = null;
+      int index = 0;
+      boolean more;
+      do {
+        if (index == expectedIndex) {
+          result = depth + 1 == path.length() ? readScalar(reader, context)
+              : readPath(reader, path, depth + 1, context);
+        } else {
+          skipValue(reader, context);
+        }
+        index++;
+        more = reader.consumeCommaOrEndArray();
+      } while (more);
+      countToken(context);
+      return result;
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  @Nullable
+  private static Object readScalar(JsonReader reader, PathContext context) {
+    char token = reader.peekToken();
+    if (token == '"') {
+      countToken(context);
+      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') {
+      countToken(context);
+      return reader.readBoolean();
+    }
+    if (token == 'n') {
+      countToken(context);
+      reader.readNull();
+      return null;
+    }
+    if (token == '{' || token == '[') {
+      // Query scalar coercion has observable error/default behavior for 
containers. Fully consume the value to keep
+      // malformed-tail and duplicate-key semantics, then signal a 
stack-trace-free retry through Jayway.
+      skipValue(reader, context);
+      return FALLBACK_REQUIRED;
+    }
+    countToken(context);
+    int start = reader.position();
+    Number value = reader.readNumber();
+    if (reader.position() - start > JACKSON_CONSTRAINTS.getMaxNumberLength()) {
+      throw new IllegalArgumentException("JSON number exceeds Jackson's 
configured limit");
+    }
+    return value;
+  }
+
+  private static void skipValue(JsonReader reader, PathContext context) {
+    char token = reader.peekToken();
+    if (token == '{') {
+      skipObject(reader, context);
+      return;
+    }
+    if (token == '[') {
+      skipArray(reader, context);
+      return;
+    }
+    if (token == '"') {
+      countToken(context);
+      // Fory 1.6's skipValue() computes an FNV hash over every character. Its 
string decoder uses packed scans and
+      // is substantially faster even when the decoded value is discarded. An 
upstream fast-skip API could remove
+      // this temporary allocation in a future Fory version.
+      String value = reader.readString();
+      if (value.length() > JACKSON_CONSTRAINTS.getMaxStringLength()) {
+        throw new IllegalArgumentException("JSON string exceeds Jackson's 
configured limit");
+      }
+      return;
+    }
+    countToken(context);
+    int start = reader.position();
+    reader.skipValue();
+    int rawLength = reader.position() - start;
+    if (token != 't' && token != 'f' && token != 'n'
+        && rawLength > JACKSON_CONSTRAINTS.getMaxNumberLength()) {
+      throw new IllegalArgumentException("JSON number exceeds Jackson's 
configured limit");
+    }
+  }
+
+  private static void skipObject(JsonReader reader, PathContext context) {
+    reader.enterDepth();
+    try {
+      countToken(context);
+      reader.expect('{');
+      if (reader.consume('}')) {
+        countToken(context);
+        return;
+      }
+      boolean more;
+      do {
+        countToken(context);
+        String fieldName = reader.readFieldName();
+        if (fieldName.length() > JACKSON_CONSTRAINTS.getMaxNameLength()) {
+          throw new IllegalArgumentException("JSON field name exceeds 
Jackson's configured limit");
+        }
+        reader.expect(':');
+        skipValue(reader, context);
+        more = reader.consumeCommaOrEndObject();
+      } while (more);
+      countToken(context);
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  private static void skipArray(JsonReader reader, PathContext context) {
+    reader.enterDepth();
+    try {
+      countToken(context);
+      reader.expect('[');
+      if (reader.consume(']')) {
+        countToken(context);
+        return;
+      }
+      boolean more;
+      do {
+        skipValue(reader, context);
+        more = reader.consumeCommaOrEndArray();
+      } while (more);
+      countToken(context);
+    } finally {
+      reader.exitDepth();
+    }
+  }
+
+  private static final class PathContext {
+    private final PathResult _marker = new PathResult();
+    private boolean _active;
+    private long _tokenCount;
+    @Nullable
+    private SimpleJsonPath _path;
+    @Nullable
+    private Object _result;
+  }
+
+  private static final class PathResult {
+  }
+
+  private static final class PathCodec implements JsonValueCodec<PathResult> {
+    private static final PathCodec INSTANCE = new PathCodec();
+
+    @Override
+    public PathResult readLatin1(Latin1JsonReader reader) {
+      return read(reader);
+    }
+
+    @Override
+    public PathResult readUtf16(Utf16JsonReader reader) {
+      return read(reader);
+    }
+
+    @Override
+    public PathResult readUtf8(Utf8JsonReader reader) {
+      return read(reader);
+    }
+
+    private static PathResult read(JsonReader reader) {
+      PathContext context = PATH_CONTEXT.get();
+      SimpleJsonPath path = context._path;
+      if (!context._active || path == null) {
+        throw new IllegalStateException("Missing JSON path extraction 
context");
+      }
+      context._result = readPath(reader, path, 0, context);
+      return context._marker;
+    }
+
+    @Override
+    public void writeString(StringJsonWriter writer, PathResult value) {
+      throw new UnsupportedOperationException("PathResult is read-only");
+    }
+
+    @Override
+    public void writeUtf8(Utf8JsonWriter writer, PathResult value) {
+      throw new UnsupportedOperationException("PathResult is read-only");
+    }
+  }
+
+  private static void countToken(PathContext context) {
+    if (JACKSON_CONSTRAINTS.hasMaxTokenCount()
+        && ++context._tokenCount > JACKSON_CONSTRAINTS.getMaxTokenCount()) {
+      throw new IllegalArgumentException("JSON token count exceeds Jackson's 
configured limit");
+    }
+  }
+}
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/TransformFunctionType.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/TransformFunctionType.java
index e884e526699..23c972d2684 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/TransformFunctionType.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/TransformFunctionType.java
@@ -107,6 +107,8 @@ public enum TransformFunctionType {
       TransformFunctionType::jsonExtractScalarReturnTypeInference, 
jsonExtractScalarOperandTypeChecker()),
   JSON_EXTRACT_SCALAR_FIRST_MATCH("jsonExtractScalarFirstMatch",
       TransformFunctionType::jsonExtractScalarReturnTypeInference, 
jsonExtractScalarOperandTypeChecker()),
+  JSON_EXTRACT_SCALAR_FORY("jsonExtractScalarFory",
+      TransformFunctionType::jsonExtractScalarReturnTypeInference, 
jsonExtractScalarOperandTypeChecker()),
   JSON_EXTRACT_INDEX("jsonExtractIndex",
       opBinding -> positionalReturnTypeInferenceFromStringLiteral(opBinding, 
2, SqlTypeName.VARCHAR),
       OperandTypes.family(
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
index df409185e4b..c8649a8add1 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
@@ -42,6 +42,7 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.common.function.FastJsonPathExtractor;
+import org.apache.pinot.common.function.ForyJsonPathExtractor;
 import org.apache.pinot.common.function.JsonPathCache;
 import org.apache.pinot.common.function.SimpleJsonPath;
 import org.apache.pinot.spi.annotations.ScalarFunction;
@@ -129,6 +130,29 @@ public class JsonFunctions {
     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) {
+    if (!(object instanceof String)) {
+      return jsonPath(object, jsonPath);
+    }
+    SimpleJsonPath simpleJsonPath = SimpleJsonPath.compile(jsonPath);
+    if (simpleJsonPath == null) {
+      return jsonPath(object, jsonPath);
+    }
+    try {
+      if (!ForyJsonPathExtractor.isAvailable()) {
+        return jsonPath(object, jsonPath);
+      }
+      Object value = ForyJsonPathExtractor.extract((String) object, 
simpleJsonPath);
+      return ForyJsonPathExtractor.isFallbackRequired(value) ? 
jsonPath(object, jsonPath) : value;
+    } catch (RuntimeException | LinkageError e) {
+      return jsonPath(object, jsonPath);
+    }
+  }
+
   /// Returns `false` when the input is known to be non-extractable by a json 
path without invoking the JSON
   /// parser: a `null` value, or a string whose first non-whitespace character 
cannot begin a JSON value. Used
   /// by the default-value `jsonPath*` overloads to skip parsing plain-text 
input (e.g. raw log lines) that
@@ -278,6 +302,24 @@ public class JsonFunctions {
     }
   }
 
+  /// **Experimental.** Fory-backed variant of [#jsonPathString(Object, 
String, String)]. Fory parses JSON strings
+  /// for simple paths; unsupported inputs and parse failures fall back to 
Jayway. This function can change or be
+  /// removed while the Fory integration is evaluated and must be explicitly 
selected by name. The optional
+  /// `org.apache.fory:fory-json:1.6.0` runtime must be on the application 
classpath to activate Fory; otherwise this
+  /// function uses Jayway.
+  @ScalarFunction(nullableParameters = true)
+  public static String jsonPathStringFory(@Nullable Object object, String 
jsonPath, String defaultValue) {
+    if (!canExtractJsonPath(object)) {
+      return defaultValue;
+    }
+    try {
+      Object jsonValue = foryJsonPath(object, jsonPath);
+      return jsonValue != null ? jsonValueToString(jsonValue) : defaultValue;
+    } catch (Exception ignore) {
+      return defaultValue;
+    }
+  }
+
   /// Extract from Json with path to Long
   @ScalarFunction
   public static long jsonPathLong(Object object, String jsonPath) {
@@ -352,6 +394,22 @@ public class JsonFunctions {
     }
   }
 
+  /// **Experimental.** Fory-backed variant of [#jsonPathLong(Object, String, 
long)]. Unsupported inputs and parse
+  /// failures fall back to Jayway. This function can change or be removed 
while the Fory integration is evaluated.
+  /// The optional `org.apache.fory:fory-json:1.6.0` runtime must be on the 
application classpath to activate Fory.
+  @ScalarFunction(nullableParameters = true)
+  public static long jsonPathLongFory(@Nullable Object object, String 
jsonPath, long defaultValue) {
+    if (!canExtractJsonPath(object)) {
+      return defaultValue;
+    }
+    try {
+      Object jsonValue = foryJsonPath(object, jsonPath);
+      return jsonValue != null ? jsonValueToLong(jsonValue) : defaultValue;
+    } catch (Exception ignore) {
+      return defaultValue;
+    }
+  }
+
   /// Extract from Json with path to Double
   @ScalarFunction
   public static double jsonPathDouble(Object object, String jsonPath) {
@@ -425,6 +483,23 @@ public class JsonFunctions {
     }
   }
 
+  /// **Experimental.** Fory-backed variant of [#jsonPathDouble(Object, 
String, double)]. Unsupported inputs and
+  /// parse failures fall back to Jayway. This function can change or be 
removed while the Fory integration is
+  /// evaluated. The optional `org.apache.fory:fory-json:1.6.0` runtime must 
be on the application classpath to
+  /// activate Fory.
+  @ScalarFunction(nullableParameters = true)
+  public static double jsonPathDoubleFory(@Nullable Object object, String 
jsonPath, double defaultValue) {
+    if (!canExtractJsonPath(object)) {
+      return defaultValue;
+    }
+    try {
+      Object jsonValue = foryJsonPath(object, jsonPath);
+      return jsonValue != null ? jsonValueToDouble(jsonValue) : defaultValue;
+    } catch (Exception ignore) {
+      return defaultValue;
+    }
+  }
+
   /// Extract an array of key-value maps to a map.
   /// E.g. input: \[{"key": "k1", "value": "v1"}, {"key": "k2", "value": 
"v2"}, {"key": "k3", "value": "v3"}\]
   ///      output: {"k1": "v1", "k2": "v2", "k3": "v3"}
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/ParserUtils.java 
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/ParserUtils.java
index 77489f847ca..67c9bc8d67d 100644
--- a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/ParserUtils.java
+++ b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/ParserUtils.java
@@ -37,6 +37,9 @@ public class ParserUtils {
       case "jsonextractscalarfirstmatch":
         validateJsonExtractScalarFunction("jsonExtractScalarFirstMatch", 
operands);
         break;
+      case "jsonextractscalarfory":
+        validateJsonExtractScalarFunction("jsonExtractScalarFory", operands);
+        break;
       case "jsonextractkey":
         validateJsonExtractKeyFunction(operands);
         break;
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java
index 3212b705d1b..f4677acb672 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java
@@ -397,6 +397,7 @@ public class FastJsonPathExtractorTest {
     assertEquals(invoke("jsonPathStringFast", json, "$.user.country", 
"DEFAULT"), "US");
     assertEquals(invoke("jsonPathStringFirstMatch", json, "$.user.country", 
"DEFAULT"), "US");
     assertEquals(invoke("jsonPathStringFast", json, "$.missing", "DEFAULT"), 
"DEFAULT");
+    assertEquals(invoke("jsonPathLongFast", json, "$.user.age", -7L), 41L);
     assertEquals(invoke("jsonPathLongFirstMatch", json, "$.user.age", -7L), 
41L);
     assertEquals(invoke("jsonPathDoubleFast", json, "$.user.score", -7.5d), 
9.5d);
     /// A complex path must still resolve through the function by falling back 
to Jayway, i.e. produce exactly
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonLinkageFallbackTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonLinkageFallbackTest.java
new file mode 100644
index 00000000000..2169d553724
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonLinkageFallbackTest.java
@@ -0,0 +1,143 @@
+/**
+ * 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.io.File;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.StringJoiner;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import org.apache.pinot.common.function.scalar.JsonFunctions;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// Verifies that an optional Fory runtime linkage failure does not break the 
JSON functions.
+public class ForyJsonLinkageFallbackTest {
+  private static final String FALLBACK_ARGUMENT = "verifyFallback";
+  private static final String TOKEN_LIMIT_ARGUMENT = "verifyTokenLimit";
+
+  /// Child-process entry point used by the missing-Fory fallback tests.
+  public static void main(String[] arguments) {
+    if (arguments.length != 1) {
+      throw new IllegalArgumentException("Expected one child-process 
verification argument");
+    }
+    if (TOKEN_LIMIT_ARGUMENT.equals(arguments[0])) {
+      verifyConfiguredTokenLimit();
+      return;
+    }
+    if (!FALLBACK_ARGUMENT.equals(arguments[0])) {
+      throw new IllegalArgumentException("Unknown child-process verification 
argument: " + arguments[0]);
+    }
+    String actual = JsonFunctions.jsonPathStringFory("{\"v\":7}", "$.v", 
"DEFAULT");
+    if (!"7".equals(actual)) {
+      throw new AssertionError("Expected Jayway fallback result 7, got: " + 
actual);
+    }
+    long longValue = JsonFunctions.jsonPathLongFory("{\"v\":7}", "$.v", -1L);
+    if (longValue != 7L) {
+      throw new AssertionError("Expected Jayway fallback long 7, got: " + 
longValue);
+    }
+    double doubleValue = JsonFunctions.jsonPathDoubleFory("{\"v\":7.5}", 
"$.v", -1d);
+    if (doubleValue != 7.5d) {
+      throw new AssertionError("Expected Jayway fallback double 7.5, got: " + 
doubleValue);
+    }
+  }
+
+  @Test
+  public void testMissingForyCoreFallsBack()
+      throws Exception {
+    runChild(false, true, FALLBACK_ARGUMENT);
+  }
+
+  @Test
+  public void testMissingForyJsonFallsBack()
+      throws Exception {
+    runChild(true, false, FALLBACK_ARGUMENT);
+  }
+
+  @Test
+  public void testConfiguredJacksonTokenLimit()
+      throws Exception {
+    runChild(false, false, TOKEN_LIMIT_ARGUMENT);
+  }
+
+  private static void verifyConfiguredTokenLimit() {
+    StreamReadConstraints constraints = 
StreamReadConstraints.builder().maxTokenCount(3).build();
+    StreamReadConstraints.overrideDefaultStreamReadConstraints(constraints);
+    SimpleJsonPath path = SimpleJsonPath.compile("$.v");
+    if (path == null) {
+      throw new AssertionError("Expected a simple JSON path");
+    }
+    try {
+      ForyJsonPathExtractor.extract("{\"v\":7}", path);
+      throw new AssertionError("Expected Fory to enforce Jackson's configured 
token limit");
+    } catch (IllegalArgumentException expected) {
+      // Expected: START_OBJECT, FIELD_NAME, VALUE_NUMBER_INT, END_OBJECT 
exceeds the configured limit of three.
+    }
+  }
+
+  private static void runChild(boolean removeForyJson, boolean removeForyCore, 
String childArgument)
+      throws Exception {
+    String separator = System.getProperty("path.separator");
+    StringJoiner childClassPath = new StringJoiner(separator);
+    boolean foundForyCore = false;
+    boolean foundForyJson = false;
+    for (String entry : 
System.getProperty("java.class.path").split(Pattern.quote(separator))) {
+      String fileName = new File(entry).getName();
+      if (fileName.startsWith("fory-core-")) {
+        foundForyCore = true;
+        if (!removeForyCore) {
+          childClassPath.add(entry);
+        }
+      } else if (fileName.startsWith("fory-json-")) {
+        foundForyJson = true;
+        if (!removeForyJson) {
+          childClassPath.add(entry);
+        }
+      } else {
+        childClassPath.add(entry);
+      }
+    }
+    assertTrue(foundForyCore, "Test classpath does not contain fory-core");
+    assertTrue(foundForyJson, "Test classpath does not contain fory-json");
+
+    String javaExecutable = new File(new File(System.getProperty("java.home"), 
"bin"), "java").getPath();
+    ProcessBuilder processBuilder = new ProcessBuilder(javaExecutable, "-cp", 
childClassPath.toString(),
+        ForyJsonLinkageFallbackTest.class.getName(), 
childArgument).redirectErrorStream(true);
+    processBuilder.environment().remove("JAVA_TOOL_OPTIONS");
+    processBuilder.environment().remove("JDK_JAVA_OPTIONS");
+
+    Process process = processBuilder.start();
+    boolean exited = process.waitFor(30, TimeUnit.SECONDS);
+    if (!exited) {
+      process.destroyForcibly();
+      process.waitFor(30, TimeUnit.SECONDS);
+    }
+    String output;
+    try (InputStream input = process.getInputStream()) {
+      output = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+    }
+    assertTrue(exited, "Fallback child process timed out: " + output);
+    assertEquals(process.exitValue(), 0, output);
+  }
+}
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonPathFunctionsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonPathFunctionsTest.java
new file mode 100644
index 00000000000..b57aadf7b9c
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/ForyJsonPathFunctionsTest.java
@@ -0,0 +1,298 @@
+/**
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.function.scalar.JsonFunctions;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+
+
+/// Differential and registry coverage for the opt-in Fory-backed JSON path 
scalar functions.
+public class ForyJsonPathFunctionsTest {
+
+  @DataProvider(name = "stringCases")
+  public Object[][] stringCases() {
+    return new Object[][]{
+        {"{\"user\":{\"country\":\"US\"}}", "$.user.country"},
+        {"{\"items\":[{\"sku\":\"A1\"},{\"sku\":\"B7\"}]}", "$.items[1].sku"},
+        {"{\"v\":2147483648}", "$.v"},
+        {"{\"v\":9223372036854775808}", "$.v"},
+        {"{\"v\":1.2345678901234567}", "$.v"},
+        {"{\"v\":123456789012345678901234567890.123}", "$.v"},
+        {"{\"v\":true}", "$.v"},
+        {"{\"v\":\"café 😀\"}", "$.v"},
+        {"{\"v\":[1,2,3]}", "$.v"},
+        {"{\"v\":{\"nested\":1}}", "$.v"},
+        {"{\"v\":1,\"v\":2}", "$.v"}
+    };
+  }
+
+  @Test(dataProvider = "stringCases")
+  public void testStringParity(String json, String path) {
+    assertEquals(JsonFunctions.jsonPathStringFory(json, path, "DEFAULT"),
+        JsonFunctions.jsonPathString(json, path, "DEFAULT"));
+  }
+
+  @Test
+  public void testTypedFunctionsAndDefaults() {
+    String json = 
"{\"n\":9223372036854775806,\"d\":19.75,\"numeric\":\"41\",\"nil\":null}";
+    assertEquals(JsonFunctions.jsonPathLongFory(json, "$.n", -1L),
+        JsonFunctions.jsonPathLong(json, "$.n", -1L));
+    assertEquals(JsonFunctions.jsonPathLongFory(json, "$.numeric", -1L), 41L);
+    assertEquals(JsonFunctions.jsonPathDoubleFory(json, "$.d", -1d),
+        JsonFunctions.jsonPathDouble(json, "$.d", -1d));
+    assertEquals(JsonFunctions.jsonPathStringFory(json, "$.missing", 
"DEFAULT"), "DEFAULT");
+    assertEquals(JsonFunctions.jsonPathStringFory(json, "$.nil", "DEFAULT"), 
"DEFAULT");
+    assertEquals(JsonFunctions.jsonPathLongFory("plain text", "$.n", -7L), 
-7L);
+    assertEquals(JsonFunctions.jsonPathDoubleFory("{broken", "$.d", -7.5d), 
-7.5d);
+    assertEquals(JsonFunctions.jsonPathStringFory(null, "$.v", "DEFAULT"), 
"DEFAULT");
+  }
+
+  @Test
+  public void testStreamingParserIsAvailable() {
+    assertTrue(ForyJsonPathExtractor.isAvailable());
+    SimpleJsonPath path = SimpleJsonPath.compile("$.nested.value");
+    assertNotNull(path);
+    
assertEquals(ForyJsonPathExtractor.extract("{\"n\":7,\"nested\":{\"value\":8}}",
 path), 8L);
+  }
+
+  @Test
+  public void testJaywayFallbacks() {
+    String complex = 
"{\"left\":{\"country\":\"US\"},\"right\":{\"country\":\"DE\"}}";
+    assertEquals(JsonFunctions.jsonPathStringFory(complex, "$..country", 
"DEFAULT"),
+        JsonFunctions.jsonPathString(complex, "$..country", "DEFAULT"));
+
+    String trailingContent = "{\"v\":7} {\"ignored\":true}";
+    assertEquals(JsonFunctions.jsonPathStringFory(trailingContent, "$.v", 
"DEFAULT"),
+        JsonFunctions.jsonPathString(trailingContent, "$.v", "DEFAULT"));
+
+    String malformedAfterMatch = "{\"v\":7,\"broken\":[}";
+    assertEquals(JsonFunctions.jsonPathStringFory(malformedAfterMatch, "$.v", 
"DEFAULT"),
+        JsonFunctions.jsonPathString(malformedAfterMatch, "$.v", "DEFAULT"));
+
+    Map<String, Object> parsed = Map.of("v", Map.of("n", 42));
+    assertEquals(JsonFunctions.jsonPathLongFory(parsed, "$.v.n", -1L),
+        JsonFunctions.jsonPathLong(parsed, "$.v.n", -1L));
+
+    StringBuilder deepJson = new StringBuilder();
+    StringBuilder deepPath = new StringBuilder("$");
+    for (int i = 0; i < 25; i++) {
+      deepJson.append("{\"a\":");
+      deepPath.append(".a");
+    }
+    deepJson.append("\"value\"");
+    for (int i = 0; i < 25; i++) {
+      deepJson.append('}');
+    }
+    assertEquals(JsonFunctions.jsonPathStringFory(deepJson.toString(), 
deepPath.toString(), "DEFAULT"), "value");
+  }
+
+  @Test
+  public void testJacksonConstraintFallbacks() {
+    StreamReadConstraints constraints = StreamReadConstraints.defaults();
+
+    String oversizedNumber = "1".repeat(constraints.getMaxNumberLength() + 1);
+    String numberDocument = "{\"oversized\":" + oversizedNumber + ",\"v\":7}";
+    assertEquals(JsonFunctions.jsonPathLongFory(numberDocument, "$.v", -1),
+        JsonFunctions.jsonPathLong(numberDocument, "$.v", -1));
+
+    String oversizedName = "n".repeat(constraints.getMaxNameLength() + 1);
+    String nameDocument = "{\"" + oversizedName + "\":1,\"v\":7}";
+    assertEquals(JsonFunctions.jsonPathLongFory(nameDocument, "$.v", -1),
+        JsonFunctions.jsonPathLong(nameDocument, "$.v", -1));
+  }
+
+  @Test
+  public void testStreamingExtractorNavigatesSimplePaths() {
+    assertEquals(extract("[{\"v\":1},{\"v\":2}]", "$[1].v"), 2L);
+    assertEquals(extract("{\"items\":[0,{\"detail\":{\"value\":3}}]}", 
"$.items[1].detail.value"), 3L);
+    assertEquals(extract("{\"a-b\":{\"café\":4}}", "$['a-b'].café"), 4L);
+  }
+
+  @Test
+  public void testStreamingExtractorUsesLastDuplicate() {
+    assertEquals(extract("{\"v\":1,\"v\":2}", "$.v"), 2L);
+    assertEquals(extract("{\"a\":{\"v\":1},\"a\":{\"v\":2}}", "$.a.v"), 2L);
+    assertNull(extract("{\"a\":{\"v\":1},\"a\":7}", "$.a.v"));
+    assertNull(extract("{\"v\":1,\"v\":null}", "$.v"));
+  }
+
+  @Test
+  public void testStreamingExtractorReturnsNullForUnresolvedPaths() {
+    assertNull(extract("{\"a\":{\"v\":1}}", "$.missing"));
+    assertNull(extract("{\"a\":{\"v\":null}}", "$.a.v"));
+    assertNull(extract("{\"a\":7}", "$.a.v"));
+    assertNull(extract("{\"a\":[]}", "$.a[1]"));
+  }
+
+  @Test
+  public void 
testStreamingExtractorSignalsContainerFallbackWithoutExceptions() {
+    
assertTrue(ForyJsonPathExtractor.isFallbackRequired(extract("{\"v\":{\"n\":1}}",
 "$.v")));
+    
assertTrue(ForyJsonPathExtractor.isFallbackRequired(extract("{\"v\":[1,2]}", 
"$.v")));
+    assertEquals(extract("{\"v\":{},\"v\":2}", "$.v"), 2L);
+    
assertTrue(ForyJsonPathExtractor.isFallbackRequired(extract("{\"v\":2,\"v\":{}}",
 "$.v")));
+    assertNull(extract("{\"v\":{},\"v\":null}", "$.v"));
+
+    SimpleJsonPath path = SimpleJsonPath.compile("$.v");
+    assertNotNull(path);
+    assertThrows(RuntimeException.class,
+        () -> ForyJsonPathExtractor.extract("{\"v\":{},\"broken\":[}", path));
+    assertEquals(ForyJsonPathExtractor.extract("{\"v\":3}", path), 3L);
+  }
+
+  @Test
+  public void testStreamingExtractorUsesJacksonNestingLimit() {
+    int depth = 25;
+    StringBuilder selectedJson = new StringBuilder();
+    StringBuilder selectedPath = new StringBuilder("$");
+    for (int i = 0; i < depth; i++) {
+      selectedJson.append("{\"a\":");
+      selectedPath.append(".a");
+    }
+    selectedJson.append('7');
+    selectedJson.append("}".repeat(depth));
+    assertEquals(extract(selectedJson.toString(), selectedPath.toString()), 
7L);
+
+    String unrelated = "{\"selected\":1,\"deep\":" + "{\"a\":".repeat(depth) + 
"7" + "}".repeat(depth)
+        + "}";
+    assertEquals(extract(unrelated, "$.selected"), 1L);
+
+    int maximumDepth = StreamReadConstraints.defaults().getMaxNestingDepth();
+    String maximumJson = "{\"a\":".repeat(maximumDepth) + "7" + 
"}".repeat(maximumDepth);
+    assertEquals(extract(maximumJson, "$." + "a.".repeat(maximumDepth - 1) + 
"a"), 7L);
+
+    String oversizedJson = "{\"a\":".repeat(maximumDepth + 1) + "7" + 
"}".repeat(maximumDepth + 1);
+    SimpleJsonPath oversizedPath = SimpleJsonPath.compile("$." + 
"a.".repeat(maximumDepth) + "a");
+    assertNotNull(oversizedPath);
+    assertThrows(RuntimeException.class, () -> 
ForyJsonPathExtractor.extract(oversizedJson, oversizedPath));
+  }
+
+  @Test
+  public void testStreamingExtractorRejectsMalformedAndTrailingContent() {
+    SimpleJsonPath path = SimpleJsonPath.compile("$.v");
+    assertNotNull(path);
+    assertThrows(RuntimeException.class, () -> 
ForyJsonPathExtractor.extract("{\"v\":1,\"broken\":[}", path));
+    assertEquals(ForyJsonPathExtractor.extract("{\"v\":2}", path), 2L);
+    assertThrows(RuntimeException.class, () -> 
ForyJsonPathExtractor.extract("{\"v\":1} {\"ignored\":2}", path));
+    assertEquals(ForyJsonPathExtractor.extract("{\"v\":3}", path), 3L);
+  }
+
+  @Test
+  public void testStreamingExtractorJacksonConstraintBoundaries() {
+    StreamReadConstraints constraints = StreamReadConstraints.defaults();
+    SimpleJsonPath valuePath = SimpleJsonPath.compile("$.v");
+    assertNotNull(valuePath);
+
+    String maximumNumber = "1".repeat(constraints.getMaxNumberLength());
+    assertEquals(ForyJsonPathExtractor.extract("{\"v\":" + maximumNumber + 
"}", valuePath).toString(),
+        maximumNumber);
+    String oversizedNumber = maximumNumber + '1';
+    assertThrows(RuntimeException.class,
+        () -> ForyJsonPathExtractor.extract("{\"v\":" + oversizedNumber + "}", 
valuePath));
+    assertThrows(RuntimeException.class,
+        () -> ForyJsonPathExtractor.extract("{\"oversized\":" + 
oversizedNumber + ",\"v\":7}", valuePath));
+
+    String maximumName = "n".repeat(constraints.getMaxNameLength());
+    assertEquals(ForyJsonPathExtractor.extract("{\"" + maximumName + 
"\":1,\"v\":7}", valuePath), 7L);
+    String oversizedName = maximumName + 'n';
+    assertThrows(RuntimeException.class,
+        () -> ForyJsonPathExtractor.extract("{\"" + oversizedName + 
"\":1,\"v\":7}", valuePath));
+  }
+
+  @Test
+  public void testStreamingExtractorDoesNotLeakContextAcrossThreads()
+      throws Exception {
+    int numThreads = 8;
+    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
+    CountDownLatch ready = new CountDownLatch(numThreads);
+    CountDownLatch start = new CountDownLatch(1);
+    try {
+      List<Future<?>> futures = new ArrayList<>(numThreads);
+      for (int worker = 0; worker < numThreads; worker++) {
+        int expected = worker;
+        futures.add(executor.submit(() -> {
+          SimpleJsonPath workerPath = SimpleJsonPath.compile("$.worker");
+          SimpleJsonPath nestedPath = 
SimpleJsonPath.compile("$.nested.worker");
+          assertNotNull(workerPath);
+          assertNotNull(nestedPath);
+          String json = "{\"worker\":" + expected + ",\"nested\":{\"worker\":" 
+ (expected + 100) + "}}";
+          ready.countDown();
+          assertTrue(start.await(10, TimeUnit.SECONDS));
+          for (int iteration = 0; iteration < 100; iteration++) {
+            if ((iteration & 1) == 0) {
+              assertEquals(ForyJsonPathExtractor.extract(json, workerPath), 
(long) expected);
+            } else {
+              assertEquals(ForyJsonPathExtractor.extract(json, nestedPath), 
(long) expected + 100);
+            }
+          }
+          return null;
+        }));
+      }
+      assertTrue(ready.await(10, TimeUnit.SECONDS));
+      start.countDown();
+      for (Future<?> future : futures) {
+        future.get(30, TimeUnit.SECONDS);
+      }
+    } finally {
+      executor.shutdownNow();
+      executor.awaitTermination(10, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testFunctionsResolveThroughRegistry()
+      throws Exception {
+    String json = "{\"user\":{\"country\":\"US\",\"age\":41,\"score\":9.5}}";
+    assertEquals(invoke("jsonPathStringFory", json, "$.user.country", 
"DEFAULT"), "US");
+    assertEquals(invoke("jsonPathLongFory", json, "$.user.age", -7L), 41L);
+    assertEquals(invoke("jsonPathDoubleFory", json, "$.user.score", -7.5d), 
9.5d);
+  }
+
+  private static Object invoke(String name, Object... arguments)
+      throws Exception {
+    FunctionInfo functionInfo =
+        
FunctionRegistry.lookupFunctionInfo(FunctionRegistry.canonicalize(name), 
arguments.length);
+    assertNotNull(functionInfo, name + "/" + arguments.length + " is not 
registered");
+    FunctionInvoker invoker = new FunctionInvoker(functionInfo);
+    Object[] copy = arguments.clone();
+    invoker.convertTypes(copy);
+    return invoker.invoke(copy);
+  }
+
+  private static Object extract(String json, String jsonPath) {
+    SimpleJsonPath path = SimpleJsonPath.compile(jsonPath);
+    assertNotNull(path);
+    return ForyJsonPathExtractor.extract(json, path);
+  }
+}
diff --git a/pinot-core/pom.xml b/pinot-core/pom.xml
index 7035b80f5bc..659781960cd 100644
--- a/pinot-core/pom.xml
+++ b/pinot-core/pom.xml
@@ -170,6 +170,11 @@
       <artifactId>assertj-core</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.fory</groupId>
+      <artifactId>fory-json</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <profiles>
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
index 21e0f3e0cde..78b054cb470 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
@@ -21,6 +21,7 @@ package org.apache.pinot.core.operator.transform.function;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.annotations.VisibleForTesting;
 import com.jayway.jsonpath.Configuration;
 import com.jayway.jsonpath.JsonPath;
 import com.jayway.jsonpath.Option;
@@ -33,6 +34,7 @@ import java.util.Map;
 import java.util.function.IntFunction;
 import javax.annotation.Nullable;
 import org.apache.pinot.common.function.FastJsonPathExtractor;
+import org.apache.pinot.common.function.ForyJsonPathExtractor;
 import org.apache.pinot.common.function.JsonPathCache;
 import org.apache.pinot.common.function.SimpleJsonPath;
 import org.apache.pinot.core.operator.ColumnContext;
@@ -55,6 +57,7 @@ import org.roaringbitmap.RoaringBitmap;
 /// | `jsonExtractScalar` | Builds the existing Jayway DOM; unchanged for 
backward compatibility. |
 /// | `jsonExtractScalarFast` | Uses [FastJsonPathExtractor] and scans the 
full root value, preserving Jayway results. |
 /// | `jsonExtractScalarFirstMatch` | Uses [FastJsonPathExtractor] and stops 
when the addressed value is found. |
+/// | `jsonExtractScalarFory` | Experimental Fory streaming extraction with 
per-row Jayway fallback. |
 ///
 /// Each function reads a JSON document from `jsonField` for each row, 
resolves the
 /// [Stefan Goessner JsonPath](https://goessner.net/articles/JsonPath/) 
expression against it, and
@@ -72,6 +75,12 @@ import org.roaringbitmap.RoaringBitmap;
 /// 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`, `JSON`, and 
`BIG_DECIMAL`. `BYTES` input, complex
+/// paths, containers / array result types, precision-sensitive results, 
documents beyond Jackson's configured
+/// limits, and Fory failures use Jayway. Its name, supported envelope, and 
implementation can change while the
+/// integration is evaluated. Fory is an optional runtime dependency; add 
`org.apache.fory:fory-json:1.6.0` (and its
+/// transitive `fory-core` dependency) to the application classpath to 
activate this experimental path.
 ///
 /// **Arguments:**
 /// - `jsonField` — single-value `STRING` or `BYTES` column / transform 
expression containing JSON.
@@ -105,11 +114,13 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
   public static final String FUNCTION_NAME = "jsonExtractScalar";
   public static final String FAST_FUNCTION_NAME = "jsonExtractScalarFast";
   public static final String FIRST_MATCH_FUNCTION_NAME = 
"jsonExtractScalarFirstMatch";
+  public static final String FORY_FUNCTION_NAME = "jsonExtractScalarFory";
 
   private enum ExtractionMode {
     JAYWAY,
     FAST,
-    FIRST_MATCH
+    FIRST_MATCH,
+    FORY
   }
 
   // This ObjectMapper requires special configurations, hence we can't use 
pinot JsonUtils here.
@@ -134,6 +145,7 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
   private DataType _storedType;
   private Object _defaultValue;
   private boolean _defaultIsNull;
+  private boolean _foryEligible;
   private TransformResultMetadata _resultMetadata;
 
   public JsonExtractScalarTransformFunction() {
@@ -159,6 +171,13 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
     }
   }
 
+  /// Experimental Fory-backed variant of [JsonExtractScalarTransformFunction].
+  public static final class Fory extends JsonExtractScalarTransformFunction {
+    public Fory() {
+      super(FORY_FUNCTION_NAME, ExtractionMode.FORY);
+    }
+  }
+
   @Override
   public String getName() {
     return _functionName;
@@ -241,6 +260,17 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
       }
     }
     _resultMetadata = new TransformResultMetadata(_dataType, isSingleValue, 
false);
+    DataType inputDataType = firstArgument.getResultMetadata().getDataType();
+    // STRING/JSON use the BigDecimal-preserving parser because non-string 
JSON values are serialized to the output.
+    // Fory 1.6 materializes fractional numbers as Double, so enabling it here 
would lose numeric precision.
+    _foryEligible = _extractionMode == ExtractionMode.FORY && _simpleJsonPath 
!= null
+        && inputDataType == DataType.STRING && isSingleValue && _dataType != 
DataType.STRING
+        && _dataType != DataType.JSON && _dataType != DataType.BIG_DECIMAL;
+  }
+
+  @VisibleForTesting
+  boolean isForyEligible() {
+    return _foryEligible;
   }
 
   @Override
@@ -802,7 +832,7 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
     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 || _extractionMode == ExtractionMode.FORY) {
         return jaywayExtractor;
       }
       SimpleJsonPath[] paths = {_simpleJsonPath};
@@ -825,6 +855,26 @@ public class JsonExtractScalarTransformFunction extends 
BaseTransformFunction {
       if (_simpleJsonPath == null) {
         return jaywayExtractor;
       }
+      if (_extractionMode == ExtractionMode.FORY) {
+        if (!_foryEligible || useBigDecimal) {
+          return jaywayExtractor;
+        }
+        try {
+          if (!ForyJsonPathExtractor.isAvailable()) {
+            return jaywayExtractor;
+          }
+        } catch (LinkageError ignored) {
+          return jaywayExtractor;
+        }
+        return i -> {
+          try {
+            Object value = ForyJsonPathExtractor.extract(jsonStrings[i], 
_simpleJsonPath);
+            return ForyJsonPathExtractor.isFallbackRequired(value) ? 
jaywayExtractor.apply(i) : (T) value;
+          } catch (RuntimeException | LinkageError ignored) {
+            return jaywayExtractor.apply(i);
+          }
+        };
+      }
       SimpleJsonPath[] paths = {_simpleJsonPath};
       Object[] result = new Object[1];
       boolean earlyExit = _extractionMode == ExtractionMode.FIRST_MATCH;
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TransformFunctionFactory.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TransformFunctionFactory.java
index 0d49547ca90..c4647f9a5bc 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TransformFunctionFactory.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TransformFunctionFactory.java
@@ -132,6 +132,8 @@ public class TransformFunctionFactory {
         JsonExtractScalarTransformFunction.Fast.class);
     
typeToImplementation.put(TransformFunctionType.JSON_EXTRACT_SCALAR_FIRST_MATCH,
         JsonExtractScalarTransformFunction.FirstMatch.class);
+    typeToImplementation.put(TransformFunctionType.JSON_EXTRACT_SCALAR_FORY,
+        JsonExtractScalarTransformFunction.Fory.class);
     typeToImplementation.put(TransformFunctionType.JSON_EXTRACT_KEY, 
JsonExtractKeyTransformFunction.class);
     typeToImplementation.put(TransformFunctionType.TIME_CONVERT, 
TimeConversionTransformFunction.class);
     typeToImplementation.put(TransformFunctionType.DATE_TIME_CONVERT, 
DateTimeConversionTransformFunction.class);
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java
index fbe2efff8ed..bbaed22658a 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java
@@ -133,6 +133,29 @@ public class JsonFunctionsTest {
     inputs.add(new Object[]{
         "json_path_double(jsonPathString, '$.k3.sub2')", 
Lists.newArrayList("jsonPathString"), row11, 1.0
     });
+
+    // Experimental Fory variants must resolve through the ingestion evaluator 
with raw JSON strings. Parsed
+    // Map/List input would deliberately use their Jayway fallback and would 
not exercise Fory initialization.
+    GenericRow row12 = new GenericRow();
+    row12.putValue("json", "{\"text\":\"value\",\"count\":10,\"ratio\":1.25}");
+    inputs.add(new Object[]{
+        "json_path_string_fory(json, '$.text', 'DEFAULT')", 
Lists.newArrayList("json"), row12, "value"
+    });
+    inputs.add(new Object[]{
+        "json_path_long_fory(json, '$.count', -1)", 
Lists.newArrayList("json"), row12, 10L
+    });
+    inputs.add(new Object[]{
+        "json_path_double_fory(json, '$.ratio', -1.0)", 
Lists.newArrayList("json"), row12, 1.25
+    });
+    inputs.add(new Object[]{
+        "json_path_string_fast(json, '$.text', 'DEFAULT')", 
Lists.newArrayList("json"), row12, "value"
+    });
+    inputs.add(new Object[]{
+        "json_path_long_fast(json, '$.count', -1)", 
Lists.newArrayList("json"), row12, 10L
+    });
+    inputs.add(new Object[]{
+        "json_path_double_fast(json, '$.ratio', -1.0)", 
Lists.newArrayList("json"), row12, 1.25
+    });
     return inputs.toArray(new Object[0][]);
   }
 
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
index 37efd3c5b10..d1a0c076d46 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
@@ -47,7 +47,7 @@ public class FunctionRegistryTest {
       TransformFunctionType.IN, TransformFunctionType.NOT_IN, 
TransformFunctionType.IS_TRUE,
       TransformFunctionType.IS_NOT_TRUE, TransformFunctionType.IS_FALSE, 
TransformFunctionType.IS_NOT_FALSE,
       TransformFunctionType.JSON_EXTRACT_SCALAR, 
TransformFunctionType.JSON_EXTRACT_SCALAR_FAST,
-      TransformFunctionType.JSON_EXTRACT_SCALAR_FIRST_MATCH,
+      TransformFunctionType.JSON_EXTRACT_SCALAR_FIRST_MATCH, 
TransformFunctionType.JSON_EXTRACT_SCALAR_FORY,
       TransformFunctionType.JSON_EXTRACT_KEY, 
TransformFunctionType.TIME_CONVERT,
       TransformFunctionType.DATE_TIME_CONVERT_WINDOW_HOP, 
TransformFunctionType.ARRAY_LENGTH,
       TransformFunctionType.ARRAY_AVERAGE, TransformFunctionType.ARRAY_MIN, 
TransformFunctionType.ARRAY_MAX,
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
index 421ded08d9c..f32114b00e0 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.core.operator.transform.function;
 
+import com.fasterxml.jackson.core.StreamReadConstraints;
 import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -55,7 +56,8 @@ public class JsonExtractScalarTransformFunctionTest extends 
BaseTransformFunctio
   private static final String[] JSON_EXTRACT_SCALAR_FUNCTIONS = {
       JsonExtractScalarTransformFunction.FUNCTION_NAME,
       JsonExtractScalarTransformFunction.FAST_FUNCTION_NAME,
-      JsonExtractScalarTransformFunction.FIRST_MATCH_FUNCTION_NAME
+      JsonExtractScalarTransformFunction.FIRST_MATCH_FUNCTION_NAME,
+      JsonExtractScalarTransformFunction.FORY_FUNCTION_NAME
   };
 
   protected File _baseDir;
@@ -544,6 +546,8 @@ public class JsonExtractScalarTransformFunctionTest extends 
BaseTransformFunctio
         new Object[]{String.format("jsonExtractScalarFast(%s, 
\"$.store.book[0].author\", 'String')", JSON_COLUMN)},
         new Object[]{String.format("jsonExtractScalarFirstMatch(%s, 
'$.store.book[0].author', \"String\")",
             JSON_COLUMN)},
+        new Object[]{String.format("jsonExtractScalarFory(%s)", JSON_COLUMN)},
+        new Object[]{String.format("jsonExtractScalarFory(%s, 
\"$.store.book[0].author\", 'String')", JSON_COLUMN)},
         new Object[]{String.format("jsonExtractKey(%s, \"$.*\")", 
JSON_COLUMN)},
         new Object[]{String.format("json_extract_key(%s, \"$.*\")", 
JSON_COLUMN)}};
     //@formatter:on
@@ -718,6 +722,23 @@ public class JsonExtractScalarTransformFunctionTest 
extends BaseTransformFunctio
         DataType.STRING, "INT", "-1", 1);
   }
 
+  @Test
+  public void testForyFallsBackForJacksonNumberConstraint() {
+    String oversizedNumber = 
"1".repeat(StreamReadConstraints.defaults().getMaxNumberLength() + 1);
+    String json = "{\"oversized\":" + oversizedNumber + ",\"v\":7}";
+    assertJsonExtractScalar(JsonExtractScalarTransformFunction.FUNCTION_NAME, 
json, DataType.STRING, "INT", "-1", -1);
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FORY_FUNCTION_NAME, 
json, DataType.STRING, "INT", "-1",
+        -1);
+  }
+
+  @Test
+  public void testForyStreamingParserFallsBackForMalformedAndTrailingContent() 
{
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FORY_FUNCTION_NAME,
+        "{\"v\":1,\"broken\":[}", DataType.STRING, "INT", "-1", -1);
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FORY_FUNCTION_NAME,
+        "{\"v\":7} {\"ignored\":true}", DataType.STRING, "INT", "-1", 7);
+  }
+
   @Test
   public void testFastExtractionFromBytesWithBigDecimal() {
     byte[] json = "{\"label\":\"crème 
brûlée\",\"v\":12345678901234567890.123456789}"
@@ -728,6 +749,36 @@ public class JsonExtractScalarTransformFunctionTest 
extends BaseTransformFunctio
         "BIG_DECIMAL", null, expected);
     
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FIRST_MATCH_FUNCTION_NAME,
 hexEncodedJson,
         DataType.BYTES, "BIG_DECIMAL", null, expected);
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FORY_FUNCTION_NAME, 
hexEncodedJson, DataType.BYTES,
+        "BIG_DECIMAL", null, expected);
+
+    byte[] hostileJson = ("{\"v\":12345678901234567890.123456789,"
+        + "\"ignored\":1e999999999999}").getBytes(StandardCharsets.UTF_8);
+    String hostileHexEncodedJson = BytesUtils.toHexString(hostileJson);
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FAST_FUNCTION_NAME, 
hostileHexEncodedJson,
+        DataType.BYTES, "BIG_DECIMAL", null, expected);
+    
assertJsonExtractScalar(JsonExtractScalarTransformFunction.FIRST_MATCH_FUNCTION_NAME,
 hostileHexEncodedJson,
+        DataType.BYTES, "BIG_DECIMAL", null, expected);
+  }
+
+  @Test
+  public void testForyEligibilityEnvelope() {
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "LONG", true);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "DOUBLE", true);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "STRING", false);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "JSON", false);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "BIG_DECIMAL", false);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$.v", "LONG_ARRAY", false);
+    assertForyEligibility(JSON_STRING_SV_COLUMN, "$..v", "LONG", false);
+    assertForyEligibility(BYTES_SV_COLUMN, "$.v", "LONG", false);
+  }
+
+  private void assertForyEligibility(String column, String path, String 
resultType, boolean expected) {
+    ExpressionContext expression = RequestContextUtils.getExpression(
+        "jsonExtractScalarFory(" + column + ", '" + path + "', '" + resultType 
+ "')");
+    TransformFunction transformFunction = 
TransformFunctionFactory.get(expression, _dataSourceMap);
+    Assert.assertTrue(transformFunction instanceof 
JsonExtractScalarTransformFunction.Fory);
+    Assert.assertEquals(((JsonExtractScalarTransformFunction) 
transformFunction).isForyEligible(), expected);
   }
 
   @Test
diff --git a/pinot-integration-tests/pom.xml b/pinot-integration-tests/pom.xml
index 4b7904d40a8..8c26aaca0bd 100644
--- a/pinot-integration-tests/pom.xml
+++ b/pinot-integration-tests/pom.xml
@@ -552,6 +552,11 @@
       <groupId>org.apache.pinot</groupId>
       <artifactId>pinot-timeseries-planner</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.fory</groupId>
+      <artifactId>fory-json</artifactId>
+      <scope>test</scope>
+    </dependency>
 
     <dependency>
       <groupId>org.apache.pinot</groupId>
diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/JsonPathTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/JsonPathTest.java
index b5d4b79d030..29bb13ae8c1 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/JsonPathTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/JsonPathTest.java
@@ -60,12 +60,14 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
   // path (which visits every doc) return the same result set but follow 
visibly different code paths.
   private static final int NUM_DISTINCT_K1 = 100;
   private static final String MY_MAP_STR_FIELD_NAME = "myMapStr";
+  private static final String MY_MAP_NUMBER_STR_FIELD_NAME = "myMapNumberStr";
   private static final String MY_MAP_BYTES_FIELD_NAME = "myMapBytes";
   private static final String MY_MAP_STR_K1_FIELD_NAME = "myMapStr_k1";
   private static final String MY_MAP_STR_K2_FIELD_NAME = "myMapStr_k2";
   /// Derived columns that exercise the opt-in fast scalar functions through 
the ingestion transform path.
   private static final String MY_MAP_STR_K1_FAST_FIELD_NAME = 
"myMapStr_k1_fast";
   private static final String MY_MAP_STR_K1_FIRST_FIELD_NAME = 
"myMapStr_k1_first";
+  private static final String MY_MAP_STR_K1_FORY_FIELD_NAME = 
"myMapStr_k1_fory";
   private static final String COMPLEX_MAP_STR_FIELD_NAME = "complexMapStr";
   private static final String COMPLEX_MAP_STR_K3_FIELD_NAME = 
"complexMapStr_k3";
 
@@ -92,11 +94,13 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
         .setSchemaName(getTableName())
         .addSingleValueDimension("myMap", DataType.STRING)
         .addSingleValueDimension(MY_MAP_STR_FIELD_NAME, DataType.STRING)
+        .addSingleValueDimension(MY_MAP_NUMBER_STR_FIELD_NAME, DataType.STRING)
         .addSingleValueDimension(MY_MAP_BYTES_FIELD_NAME, DataType.BYTES)
         .addSingleValueDimension(MY_MAP_STR_K1_FIELD_NAME, DataType.STRING)
         .addSingleValueDimension(MY_MAP_STR_K2_FIELD_NAME, DataType.STRING)
         .addSingleValueDimension(MY_MAP_STR_K1_FAST_FIELD_NAME, 
DataType.STRING)
         .addSingleValueDimension(MY_MAP_STR_K1_FIRST_FIELD_NAME, 
DataType.STRING)
+        .addSingleValueDimension(MY_MAP_STR_K1_FORY_FIELD_NAME, 
DataType.STRING)
         .addSingleValueDimension(COMPLEX_MAP_STR_FIELD_NAME, DataType.STRING)
         .addMultiValueDimension(COMPLEX_MAP_STR_K3_FIELD_NAME, DataType.STRING)
         .build();
@@ -111,6 +115,8 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
             "jsonPathStringFast(" + MY_MAP_STR_FIELD_NAME + ", '$.k1', 
'DEFAULT')"),
         new TransformConfig(MY_MAP_STR_K1_FIRST_FIELD_NAME,
             "jsonPathStringFirstMatch(" + MY_MAP_STR_FIELD_NAME + ", '$.k1', 
'DEFAULT')"),
+        new TransformConfig(MY_MAP_STR_K1_FORY_FIELD_NAME,
+            "jsonPathStringFory(" + MY_MAP_STR_FIELD_NAME + ", '$.k1', 
'DEFAULT')"),
         new TransformConfig(COMPLEX_MAP_STR_K3_FIELD_NAME, "jsonPathArray(" + 
COMPLEX_MAP_STR_FIELD_NAME + ", '$.k3')")
     );
     IngestionConfig ingestionConfig = new IngestionConfig();
@@ -129,6 +135,8 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
     List<org.apache.avro.Schema.Field> fields = List.of(
         new org.apache.avro.Schema.Field(MY_MAP_STR_FIELD_NAME,
             org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), 
null, null),
+        new org.apache.avro.Schema.Field(MY_MAP_NUMBER_STR_FIELD_NAME,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), 
null, null),
         new org.apache.avro.Schema.Field(MY_MAP_BYTES_FIELD_NAME,
             org.apache.avro.Schema.create(org.apache.avro.Schema.Type.BYTES), 
null, null),
         new org.apache.avro.Schema.Field(COMPLEX_MAP_STR_FIELD_NAME,
@@ -144,6 +152,7 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
         GenericData.Record record = new GenericData.Record(avroSchema);
         String myMapJson = JsonUtils.objectToString(map);
         record.put(MY_MAP_STR_FIELD_NAME, myMapJson);
+        record.put(MY_MAP_NUMBER_STR_FIELD_NAME, 
JsonUtils.objectToString(Map.of("n", i)));
         record.put(MY_MAP_BYTES_FIELD_NAME, 
ByteBuffer.wrap(myMapJson.getBytes(StandardCharsets.UTF_8)));
 
         Map<String, Object> complexMap = new HashMap<>();
@@ -398,52 +407,68 @@ public class JsonPathTest extends 
CustomDataQueryClusterIntegrationTest {
     assertEquals(pinotResponse.get("totalDocs").asInt(), 0);
   }
 
-  /// End-to-end coverage for the opt-in fast functions. They take an `Object` 
argument, so - like the
-  /// existing `jsonPathString` - they are used through the ingestion 
transform path, not as query-time scalars.
-  /// `myMapStr_k1_fast` / `myMapStr_k1_first` are derived at ingestion via 
`jsonPathStringFast` /
-  /// `jsonPathStringFirstMatch`; this asserts, over real rows on both query 
engines, that they equal the
-  /// Jayway-derived `myMapStr_k1`. The rows have no duplicate keys and no 
malformed content, so `FirstMatch`
-  /// must also agree.
+  /// End-to-end coverage for the opt-in fast functions and the experimental 
Fory function. They take an `Object`
+  /// argument, so - like the existing `jsonPathString` - they are used 
through the ingestion transform path, not as
+  /// query-time scalars.
+  /// The additional columns are derived at ingestion via 
`jsonPathStringFast`, `jsonPathStringFirstMatch`, and
+  /// `jsonPathStringFory`; this asserts, over real rows on both query 
engines, that they equal the Jayway-derived
+  /// `myMapStr_k1`. The rows have no duplicate keys and no malformed content, 
so `FirstMatch` must also agree.
   @Test(dataProvider = "useBothQueryEngines")
   void testFastScalarFunctions(boolean useMultiStageQueryEngine)
       throws Exception {
     setUseMultiStageQueryEngine(useMultiStageQueryEngine);
-    String query = "SELECT myMapStr_k1, myMapStr_k1_fast, myMapStr_k1_first 
FROM " + getTableName() + " LIMIT 1000";
+    String query = "SELECT myMapStr_k1, myMapStr_k1_fast, myMapStr_k1_first, 
myMapStr_k1_fory FROM "
+        + getTableName() + " LIMIT 1000";
     JsonNode rows = postQuery(query).get("resultTable").get("rows");
     assertTrue(rows.size() > 0, "expected non-empty result set");
     for (JsonNode row : rows) {
       String jayway = row.get(0).asText();
       assertEquals(row.get(1).asText(), jayway, "jsonPathStringFast must equal 
Jayway jsonPathString");
       assertEquals(row.get(2).asText(), jayway, "jsonPathStringFirstMatch must 
equal Jayway on clean data");
+      assertEquals(row.get(3).asText(), jayway, "jsonPathStringFory must equal 
Jayway jsonPathString");
     }
   }
 
-  /// Query-time coverage for the typed transforms backed by the same fast 
extractor. Both modes must match the
-  /// existing `jsonExtractScalar` transform on this clean, duplicate-free 
data set.
+  /// Query-time coverage for the opt-in typed transforms. Each mode must 
match the existing `jsonExtractScalar`
+  /// transform on this clean, duplicate-free data set. The numeric query is 
eligible for Fory's streaming fast path;
+  /// STRING results and BYTES input deliberately exercise its Jayway 
compatibility fallback.
   @Test(dataProvider = "useBothQueryEngines")
   void testFastJsonExtractScalarTransforms(boolean useMultiStageQueryEngine)
       throws Exception {
     setUseMultiStageQueryEngine(useMultiStageQueryEngine);
     String query = "SELECT jsonExtractScalar(myMapStr, '$.k1', 'STRING'), "
         + "jsonExtractScalarFast(myMapStr, '$.k1', 'STRING'), "
-        + "jsonExtractScalarFirstMatch(myMapStr, '$.k1', 'STRING') FROM " + 
getTableName() + " LIMIT 1000";
+        + "jsonExtractScalarFirstMatch(myMapStr, '$.k1', 'STRING'), "
+        + "jsonExtractScalarFory(myMapStr, '$.k1', 'STRING') FROM " + 
getTableName() + " LIMIT 1000";
     JsonNode rows = postQuery(query).get("resultTable").get("rows");
     assertTrue(rows.size() > 0, "expected non-empty result set");
     for (JsonNode row : rows) {
       String jayway = row.get(0).asText();
       assertEquals(row.get(1).asText(), jayway, "jsonExtractScalarFast must 
equal jsonExtractScalar");
       assertEquals(row.get(2).asText(), jayway, "jsonExtractScalarFirstMatch 
must equal Jayway on clean data");
+      assertEquals(row.get(3).asText(), jayway, "jsonExtractScalarFory must 
equal Jayway on clean data");
+    }
+
+    query = "SELECT jsonExtractScalar(myMapNumberStr, '$.n', 'LONG'), "
+        + "jsonExtractScalarFory(myMapNumberStr, '$.n', 'LONG') FROM " + 
getTableName() + " LIMIT 1000";
+    rows = postQuery(query).get("resultTable").get("rows");
+    assertTrue(rows.size() > 0, "expected non-empty numeric result set");
+    for (JsonNode row : rows) {
+      assertEquals(row.get(1).asLong(), row.get(0).asLong(),
+          "streaming Fory extraction must equal jsonExtractScalar");
     }
 
     query = "SELECT jsonExtractScalar(myMapBytes, '$.k1', 'STRING'), "
         + "jsonExtractScalarFast(myMapBytes, '$.k1', 'STRING'), "
-        + "jsonExtractScalarFirstMatch(myMapBytes, '$.k1', 'STRING') FROM " + 
getTableName() + " LIMIT 1000";
+        + "jsonExtractScalarFirstMatch(myMapBytes, '$.k1', 'STRING'), "
+        + "jsonExtractScalarFory(myMapBytes, '$.k1', 'STRING') FROM " + 
getTableName() + " LIMIT 1000";
     rows = postQuery(query).get("resultTable").get("rows");
     assertTrue(rows.size() > 0, "expected non-empty BYTES result set");
     for (JsonNode row : rows) {
       String jayway = row.get(0).asText();
       assertEquals(row.get(1).asText(), jayway, "BYTES fast extraction must 
equal jsonExtractScalar");
       assertEquals(row.get(2).asText(), jayway, "BYTES first-match extraction 
must equal Jayway on clean data");
+      assertEquals(row.get(3).asText(), jayway, "BYTES Fory fallback must 
equal Jayway on clean data");
     }
   }
 
diff --git 
a/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
 
b/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
index be6b3059d1c..5c44ac2f86b 100644
--- 
a/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
+++ 
b/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
@@ -997,6 +997,10 @@ jsonextractscalarfirstmatch:
   scalar: null
   transform: 
"org.apache.pinot.core.operator.transform.function.JsonExtractScalarTransformFunction.FirstMatch"
   udf: null
+jsonextractscalarfory:
+  scalar: null
+  transform: 
"org.apache.pinot.core.operator.transform.function.JsonExtractScalarTransformFunction.Fory"
+  udf: null
 jsonformat:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonFormat}"
   transform: null
@@ -1031,6 +1035,10 @@ jsonpathdoublefirstmatch:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathDoubleFirstMatch}"
   transform: null
   udf: null
+jsonpathdoublefory:
+  scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathDoubleFory}"
+  transform: null
+  udf: null
 jsonpathexists:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathExists}"
   transform: null
@@ -1048,6 +1056,10 @@ jsonpathlongfirstmatch:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathLongFirstMatch}"
   transform: null
   udf: null
+jsonpathlongfory:
+  scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathLongFory}"
+  transform: null
+  udf: null
 jsonpathstring:
   scalar: "ArgumentCountBasedScalarFunction{[2: 
org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathString,\
     \ 3: 
org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathString]}"
@@ -1061,6 +1073,10 @@ jsonpathstringfirstmatch:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathStringFirstMatch}"
   transform: null
   udf: null
+jsonpathstringfory:
+  scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonPathStringFory}"
+  transform: null
+  udf: null
 jsonstringtoarray:
   scalar: 
"ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.JsonFunctions.jsonStringToArray}"
   transform: null
diff --git a/pinot-perf/pom.xml b/pinot-perf/pom.xml
index 68fd9816503..4009800d0c2 100644
--- a/pinot-perf/pom.xml
+++ b/pinot-perf/pom.xml
@@ -113,6 +113,10 @@
       <groupId>org.openjdk.jmh</groupId>
       <artifactId>jmh-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.fory</groupId>
+      <artifactId>fory-json</artifactId>
+    </dependency>
     <dependency>
       <groupId>net.sf.jopt-simple</groupId>
       <artifactId>jopt-simple</artifactId>
diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkForyJsonFallback.java 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkForyJsonFallback.java
new file mode 100644
index 00000000000..317826fda2f
--- /dev/null
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkForyJsonFallback.java
@@ -0,0 +1,109 @@
+/**
+ * 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.concurrent.TimeUnit;
+import org.apache.pinot.common.function.ForyJsonPathExtractor;
+import org.apache.pinot.common.function.SimpleJsonPath;
+import org.apache.pinot.common.function.scalar.JsonFunctions;
+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.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;
+
+
+/// Compares normal, Fast, and Fory production scalar functions on values that 
require a reference-parser fallback
+/// and on documents beyond Fory's former depth-20 limit. Run with multiple 
JMH threads to expose parser-pool
+/// contention as well as per-row exception/fallback costs.
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(1)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 3)
+@State(Scope.Thread)
+public class BenchmarkForyJsonFallback {
+  private static final String DEFAULT_VALUE = "DEFAULT";
+
+  @Param({"object", "array", "deepSelected", "deepUnrelated"})
+  private String _scenario;
+
+  private String _json;
+  private String _path;
+
+  @Setup
+  public void setUp() {
+    if (!ForyJsonPathExtractor.isAvailable()) {
+      throw new IllegalStateException("Fory JSON is unavailable; refusing to 
publish fallback results as Fory");
+    }
+    switch (_scenario) {
+      case "object":
+        _json = "{\"v\":{\"n\":1}}";
+        _path = "$.v";
+        break;
+      case "array":
+        _json = "{\"v\":[1,2,3]}";
+        _path = "$.v";
+        break;
+      case "deepSelected":
+        _json = "{\"a\":".repeat(25) + "\"value\"" + "}".repeat(25);
+        _path = "$." + "a.".repeat(24) + "a";
+        break;
+      case "deepUnrelated":
+        _json = "{\"v\":\"value\",\"deep\":" + "{\"a\":".repeat(25) + "1" + 
"}".repeat(25) + "}";
+        _path = "$.v";
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported scenario: " + 
_scenario);
+    }
+
+    String expected = JsonFunctions.jsonPathString(_json, _path, 
DEFAULT_VALUE);
+    if (!expected.equals(JsonFunctions.jsonPathStringFast(_json, _path, 
DEFAULT_VALUE))
+        || !expected.equals(JsonFunctions.jsonPathStringFory(_json, _path, 
DEFAULT_VALUE))) {
+      throw new IllegalStateException("JSON functions disagree for scenario: " 
+ _scenario);
+    }
+    Object directResult = ForyJsonPathExtractor.extract(_json, 
SimpleJsonPath.compile(_path));
+    boolean expectedFallback = _scenario.equals("object") || 
_scenario.equals("array");
+    if (ForyJsonPathExtractor.isFallbackRequired(directResult) != 
expectedFallback
+        || !expectedFallback && !expected.equals(directResult)) {
+      throw new IllegalStateException("Fory did not directly exercise the 
expected path for scenario: " + _scenario);
+    }
+  }
+
+  @Benchmark
+  public String jsonPathStringJayway() {
+    return JsonFunctions.jsonPathString(_json, _path, DEFAULT_VALUE);
+  }
+
+  @Benchmark
+  public String jsonPathStringFast() {
+    return JsonFunctions.jsonPathStringFast(_json, _path, DEFAULT_VALUE);
+  }
+
+  @Benchmark
+  public String jsonPathStringFory() {
+    return JsonFunctions.jsonPathStringFory(_json, _path, DEFAULT_VALUE);
+  }
+}
diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java
 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java
new file mode 100644
index 00000000000..39a53ebc358
--- /dev/null
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java
@@ -0,0 +1,351 @@
+/**
+ * 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.Objects;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.function.ForyJsonPathExtractor;
+import org.apache.pinot.common.function.SimpleJsonPath;
+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.
+/// Result dispatch is selected once during setup, outside the measured 
per-row loop. The type-specific JSON literals
+/// have equal encoded lengths and occupy the same early/late field locations 
so STRING, LONG, and DOUBLE comparisons
+/// do not accidentally measure different document layouts.
+@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_FORMAT = "{"
+      + "\"earlyValue\":%s,"
+      + 
"\"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\"],"
+      + "\"lateValue\":%s} ";
+  private static final String LATE_FIELD_MARKER = "\"lateValue\":";
+
+  @Param({"early", "late"})
+  private String _fieldPosition;
+
+  @Param({"700", "8192", "65536"})
+  private int _documentBytes;
+
+  /// STRING is an intentional Jayway fallback for precision-safe coercion. 
Add `-p _resultType=STRING` explicitly to
+  /// characterize that public-function fallback; default trials cover only 
actual Fory streaming.
+  @Param({"LONG", "DOUBLE"})
+  private DataType _resultType;
+
+  /// Use `-p _pathResult=missing` to measure explicit defaults without 
multiplying the default suite.
+  @Param({"hit"})
+  private String _pathResult;
+
+  private ValueBlock _valueBlock;
+  private JsonExtractScalarTransformFunction _jayway;
+  private JsonExtractScalarTransformFunction _fast;
+  private JsonExtractScalarTransformFunction _firstMatch;
+  private JsonExtractScalarTransformFunction _fory;
+
+  @Setup
+  public void setUp() {
+    if (_resultType != DataType.STRING && 
!ForyJsonPathExtractor.isAvailable()) {
+      throw new IllegalStateException("Fory JSON is unavailable; refusing to 
publish fallback results as Fory");
+    }
+    String json = buildJson(_documentBytes, _resultType);
+    String[] jsonRows = new String[BLOCK_ROWS];
+    Arrays.fill(jsonRows, json);
+    TransformFunction input = new StringArrayTransformFunction(jsonRows);
+    boolean early = "early".equals(_fieldPosition);
+    boolean hit = "hit".equals(_pathResult);
+    String path = "$." + (early ? "early" : "late") + (hit ? "Value" : 
"Ghost");
+    Object defaultValue = defaultValue(_resultType);
+    List<TransformFunction> arguments = List.of(input, 
literal(DataType.STRING, path),
+        literal(DataType.STRING, _resultType.name()), literal(_resultType, 
defaultValue));
+
+    _valueBlock = new FixedValueBlock(BLOCK_ROWS);
+    if (_resultType != DataType.STRING) {
+      Object directForyResult = ForyJsonPathExtractor.extract(json, 
SimpleJsonPath.compile(path));
+      Object expectedForyResult = hit ? hitValue(_resultType, early) : null;
+      if (!Objects.equals(directForyResult, expectedForyResult)) {
+        throw new IllegalStateException("Direct Fory extraction produced an 
unexpected " + _resultType + " result");
+      }
+    }
+    _jayway = initialize(new JsonExtractScalarTransformFunction(), arguments);
+    _fast = initialize(new JsonExtractScalarTransformFunction.Fast(), 
arguments);
+    _firstMatch = initialize(new 
JsonExtractScalarTransformFunction.FirstMatch(), arguments);
+    _fory = initialize(new JsonExtractScalarTransformFunction.Fory(), 
arguments);
+
+    Object expectedRows = expectedRows(_resultType, hit ? 
hitValue(_resultType, early) : defaultValue);
+    Object jaywayRows = apply(_jayway);
+    assertResultsEqual("jsonExtractScalar", expectedRows, jaywayRows);
+    assertResultsEqual("jsonExtractScalarFast", jaywayRows, apply(_fast));
+    assertResultsEqual("jsonExtractScalarFirstMatch", jaywayRows, 
apply(_firstMatch));
+    assertResultsEqual("jsonExtractScalarFory", jaywayRows, apply(_fory));
+  }
+
+  private JsonExtractScalarTransformFunction 
initialize(JsonExtractScalarTransformFunction function,
+      List<TransformFunction> arguments) {
+    function.init(arguments, Map.<String, ColumnContext>of(), false);
+    return function;
+  }
+
+  private Object apply(JsonExtractScalarTransformFunction function) {
+    switch (_resultType) {
+      case STRING:
+        return function.transformToStringValuesSV(_valueBlock);
+      case LONG:
+        return function.transformToLongValuesSV(_valueBlock);
+      case DOUBLE:
+        return function.transformToDoubleValuesSV(_valueBlock);
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ _resultType);
+    }
+  }
+
+  private static LiteralTransformFunction literal(DataType dataType, Object 
value) {
+    return new LiteralTransformFunction(new LiteralContext(dataType, value));
+  }
+
+  private static String buildJson(int targetBytes, DataType resultType) {
+    String baseJson = String.format(BASE_JSON_FORMAT, jsonLiteral(resultType, 
true), jsonLiteral(resultType, false));
+    if (baseJson.length() >= targetBytes) {
+      return baseJson;
+    }
+    int markerOffset = baseJson.indexOf(LATE_FIELD_MARKER);
+    String paddingPrefix = "\"padding\":\"";
+    String paddingSuffix = "\",";
+    int paddingLength = targetBytes - baseJson.length() - 
paddingPrefix.length() - paddingSuffix.length();
+    if (paddingLength <= 0) {
+      return baseJson;
+    }
+    return baseJson.substring(0, markerOffset) + paddingPrefix + 
"x".repeat(paddingLength) + paddingSuffix
+        + baseJson.substring(markerOffset);
+  }
+
+  private static String jsonLiteral(DataType resultType, boolean early) {
+    switch (resultType) {
+      case STRING:
+        return early ? "\"S\"" : "\"T\"";
+      case LONG:
+        return early ? "170" : "190";
+      case DOUBLE:
+        return early ? "1.7" : "1.9";
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ resultType);
+    }
+  }
+
+  private static Object hitValue(DataType resultType, boolean early) {
+    switch (resultType) {
+      case STRING:
+        return early ? "S" : "T";
+      case LONG:
+        return early ? 170L : 190L;
+      case DOUBLE:
+        return early ? 1.7d : 1.9d;
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ resultType);
+    }
+  }
+
+  private static Object defaultValue(DataType resultType) {
+    switch (resultType) {
+      case STRING:
+        return "DEFAULT";
+      case LONG:
+        return -1L;
+      case DOUBLE:
+        return -1d;
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ resultType);
+    }
+  }
+
+  private static Object expectedRows(DataType resultType, Object 
expectedValue) {
+    switch (resultType) {
+      case STRING:
+        String[] stringValues = new String[BLOCK_ROWS];
+        Arrays.fill(stringValues, (String) expectedValue);
+        return stringValues;
+      case LONG:
+        long[] longValues = new long[BLOCK_ROWS];
+        Arrays.fill(longValues, (Long) expectedValue);
+        return longValues;
+      case DOUBLE:
+        double[] doubleValues = new double[BLOCK_ROWS];
+        Arrays.fill(doubleValues, (Double) expectedValue);
+        return doubleValues;
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ resultType);
+    }
+  }
+
+  private void assertResultsEqual(String functionName, Object expected, Object 
actual) {
+    boolean equal;
+    switch (_resultType) {
+      case STRING:
+        equal = Arrays.equals((String[]) expected, (String[]) actual);
+        break;
+      case LONG:
+        equal = Arrays.equals((long[]) expected, (long[]) actual);
+        break;
+      case DOUBLE:
+        equal = Arrays.equals((double[]) expected, (double[]) actual);
+        break;
+      default:
+        throw new IllegalStateException("Unsupported benchmark result type: " 
+ _resultType);
+    }
+    if (!equal) {
+      throw new IllegalStateException(functionName + " produced an unexpected 
" + _resultType + " result for "
+          + _fieldPosition + '/' + _pathResult);
+    }
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(BLOCK_ROWS)
+  public Object queryJayway() {
+    return apply(_jayway);
+  }
+
+  @Benchmark
+  @OperationsPerInvocation(BLOCK_ROWS)
+  public Object queryFast() {
+    return apply(_fast);
+  }
+
+  /// Auxiliary comparator: unlike the primary parity-preserving variants, 
FirstMatch intentionally has weaker
+  /// duplicate-key and malformed-tail semantics.
+  @Benchmark
+  @OperationsPerInvocation(BLOCK_ROWS)
+  public Object queryFirstMatch() {
+    return apply(_firstMatch);
+  }
+
+  /// For STRING, this measures the production Fory function's intentional 
Jayway fallback rather than Fory parsing.
+  @Benchmark
+  @OperationsPerInvocation(BLOCK_ROWS)
+  public Object queryFory() {
+    return apply(_fory);
+  }
+
+  public static void main(String[] arguments)
+      throws Exception {
+    Options options = new 
OptionsBuilder().include(BenchmarkJsonExtractScalarQuery.class.getSimpleName()).build();
+    new Runner(options).run();
+  }
+
+  private static final class StringArrayTransformFunction extends 
BaseTransformFunction {
+    private final String[] _values;
+
+    private StringArrayTransformFunction(String[] values) {
+      _values = values;
+    }
+
+    @Override
+    public String getName() {
+      return "stringArrayInput";
+    }
+
+    @Override
+    public TransformResultMetadata getResultMetadata() {
+      return STRING_METADATA;
+    }
+
+    @Override
+    public String[] transformToStringValuesSV(ValueBlock valueBlock) {
+      return _values;
+    }
+  }
+
+  private static final class FixedValueBlock implements ValueBlock {
+    private final int _numDocs;
+
+    private FixedValueBlock(int numDocs) {
+      _numDocs = numDocs;
+    }
+
+    @Override
+    public int getNumDocs() {
+      return _numDocs;
+    }
+
+    @Override
+    public int[] getDocIds() {
+      return null;
+    }
+
+    @Override
+    public BlockValSet getBlockValueSet(ExpressionContext expression) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BlockValSet getBlockValueSet(String column) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BlockValSet getBlockValueSet(String[] paths) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java
 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java
index effb8925962..2e1b8fa6d84 100644
--- 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java
@@ -19,6 +19,7 @@
 package org.apache.pinot.perf;
 
 import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.DocumentContext;
 import com.jayway.jsonpath.JsonPath;
 import com.jayway.jsonpath.Option;
 import com.jayway.jsonpath.ParseContext;
@@ -26,7 +27,9 @@ import com.jayway.jsonpath.Predicate;
 import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
 import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
 import java.util.concurrent.TimeUnit;
+import org.apache.fory.json.ForyJson;
 import org.apache.pinot.common.function.FastJsonPathExtractor;
+import org.apache.pinot.common.function.ForyJsonPathExtractor;
 import org.apache.pinot.common.function.SimpleJsonPath;
 import org.apache.pinot.common.function.scalar.JsonFunctions;
 import org.openjdk.jmh.annotations.Benchmark;
@@ -47,31 +50,36 @@ import org.openjdk.jmh.runner.options.OptionsBuilder;
 
 
 /// Compares JsonPath extraction through Jayway (today's implementation, which 
builds a full Jackson DOM of the
-/// document and then walks to the field) against [FastJsonPathExtractor].
+/// document and then walks to the field), Fory's streaming and dynamic-tree 
parsers, and [FastJsonPathExtractor].
 ///
 /// The `fieldPosition` parameter puts the extracted field either near the 
start or at the very end of a
-/// ~700 byte nested event payload, because that is what decides whether early 
exit can pay off.
+/// nested event payload, because that is what decides whether early exit can 
pay off. `documentBytes` pads the
+/// payload immediately before the late field so parser scaling is visible 
without changing the addressed values.
 ///
-/// The single-column benchmarks are also the per-row cost of 
`jsonExtractScalar`: that transform function
-/// does exactly `parseContext.parse(row).read(jsonPath)` per row, so 
measuring the extraction in isolation
-/// measures it without the surrounding `ValueBlock` scaffolding.
+/// The scalar-function methods measure the production `jsonPath*` ingestion 
wrappers. The lower-level single-column
+/// methods isolate parser/traversal strategies; 
[BenchmarkJsonExtractScalarQuery] is the authoritative query-transform
+/// measurement.
 @BenchmarkMode(Mode.Throughput)
 @OutputTimeUnit(TimeUnit.SECONDS)
 @Fork(1)
 @Warmup(iterations = 3, time = 2)
 @Measurement(iterations = 5, time = 3)
-@State(Scope.Benchmark)
+@State(Scope.Thread)
 public class BenchmarkJsonPathExtraction {
   private static final Predicate[] NO_PREDICATES = new Predicate[0];
+  private static final ForyJson FORY_JSON = ForyJson.builder().build();
+  private static final String STRING_DEFAULT = "DEFAULT";
+  private static final long LONG_DEFAULT = -1L;
+  private static final double DOUBLE_DEFAULT = -1.25d;
 
   /// Exactly the context `JsonExtractScalarTransformFunction` and 
`JsonFunctions` use.
   private static final ParseContext PARSE_CONTEXT = JsonPath.using(
       new Configuration.ConfigurationBuilder().jsonProvider(new 
JacksonJsonProvider())
           .mappingProvider(new 
JacksonMappingProvider()).options(Option.SUPPRESS_EXCEPTIONS).build());
 
-  private static final String JSON = "{"
+  private static final String BASE_JSON = "{"
       + "\"ts\":1719878400123,"
-      + 
"\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"tier\":\"gold\",\"age\":41},"
+      + 
"\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"score\":17.25,\"age\":41},"
       + 
"\"event\":{\"name\":\"checkout\",\"cart\":[{\"sku\":\"A1\",\"qty\":2,\"price\":19.99},"
       + 
"{\"sku\":\"B7\",\"qty\":1,\"price\":149.5},{\"sku\":\"C3\",\"qty\":5,\"price\":3.25}],"
       + "\"total\":352.73,\"currency\":\"USD\"},"
@@ -81,17 +89,30 @@ public class BenchmarkJsonPathExtraction {
       + 
"\"tags\":[\"mobile\",\"ios\",\"returning\",\"promo-eligible\",\"newsletter\"],"
       + 
"\"session\":{\"id\":\"s-aaaabbbbccccdddd\",\"start\":1719878300000,\"pages\":14,\"referrer\":"
       + "\"https://example.com/landing?utm_source=x&utm_medium=y\"},";
-      + "\"trailer\":{\"country\":\"DE\",\"note\":\"last field in the 
document\"}"
+      + "\"trailer\":{\"country\":\"DE\",\"double\":19.25,\"long\":19}"
       + "}";
 
   /// Four derived columns, spread through the document, as an ingestion 
`transformConfigs` would pull.
   private static final String[] FOUR_PATHS = {"$.user.country", 
"$.event.currency", "$.device.os", "$.geo.city"};
+  private static final String COMPLEX_PATH = "$.event.cart[*].sku";
 
   @Param({"early", "late"})
   private String _fieldPosition;
 
+  @Param({"700", "8192", "65536"})
+  private int _documentBytes;
+
+  /// Use `-p _valueCase=missing` to measure the explicit-default path without 
doubling the default benchmark suite.
+  @Param({"hit"})
+  private String _valueCase;
+
+  private String _json;
   private String _path;
+  private String _longPath;
+  private String _doublePath;
   private SimpleJsonPath _simplePath;
+  private SimpleJsonPath _simpleLongPath;
+  private SimpleJsonPath _simpleDoublePath;
   private SimpleJsonPath[] _simpleFourPaths;
   private Object[] _fourResults;
 
@@ -104,53 +125,211 @@ public class BenchmarkJsonPathExtraction {
 
   @Setup(Level.Trial)
   public void setUp() {
-    _path = "early".equals(_fieldPosition) ? "$.user.country" : 
"$.trailer.country";
+    if (!ForyJsonPathExtractor.isAvailable()) {
+      throw new IllegalStateException("Fory JSON is unavailable; refusing to 
publish fallback results as Fory");
+    }
+    _json = buildJson(_documentBytes);
+    boolean missing;
+    if ("hit".equals(_valueCase)) {
+      missing = false;
+    } else if ("missing".equals(_valueCase)) {
+      missing = true;
+    } else {
+      throw new IllegalArgumentException("Unsupported value case: " + 
_valueCase);
+    }
+    if (missing) {
+      _path = "$.missing";
+      _longPath = _path;
+      _doublePath = _path;
+    } else if ("early".equals(_fieldPosition)) {
+      _path = "$.user.country";
+      _longPath = "$.user.age";
+      _doublePath = "$.user.score";
+    } else {
+      _path = "$.trailer.country";
+      _longPath = "$.trailer.long";
+      _doublePath = "$.trailer.double";
+    }
     _simplePath = SimpleJsonPath.compile(_path);
+    _simpleLongPath = SimpleJsonPath.compile(_longPath);
+    _simpleDoublePath = SimpleJsonPath.compile(_doublePath);
     _simpleFourPaths = new SimpleJsonPath[FOUR_PATHS.length];
     for (int i = 0; i < FOUR_PATHS.length; i++) {
       _simpleFourPaths[i] = SimpleJsonPath.compile(FOUR_PATHS[i]);
     }
     _fourResults = new Object[FOUR_PATHS.length];
+
+    String expectedString = missing ? STRING_DEFAULT : 
"early".equals(_fieldPosition) ? "US" : "DE";
+    long expectedLong = missing ? LONG_DEFAULT : 
"early".equals(_fieldPosition) ? 41L : 19L;
+    double expectedDouble = missing ? DOUBLE_DEFAULT : 
"early".equals(_fieldPosition) ? 17.25d : 19.25d;
+    verifyScalarFunctions(expectedString, expectedLong, expectedDouble);
+    for (String path : FOUR_PATHS) {
+      String jayway = JsonFunctions.jsonPathString(_json, path, "");
+      String fory = JsonFunctions.jsonPathStringFory(_json, path, "");
+      if (!jayway.equals(fory)) {
+        throw new IllegalStateException("Fory result does not match Jayway for 
" + path);
+      }
+    }
+    String expectedCart = "[\"A1\",\"B7\",\"C3\"]";
+    if (!expectedCart.equals(JsonFunctions.jsonPathStringFory(_json, 
COMPLEX_PATH, ""))) {
+      throw new IllegalStateException("Complex-path fallback does not match 
the expected cart");
+    }
+  }
+
+  private void verifyScalarFunctions(String expectedString, long expectedLong, 
double expectedDouble) {
+    Object extractedString = ForyJsonPathExtractor.extract(_json, _simplePath);
+    Object extractedLong = ForyJsonPathExtractor.extract(_json, 
_simpleLongPath);
+    Object extractedDouble = ForyJsonPathExtractor.extract(_json, 
_simpleDoublePath);
+    if ("missing".equals(_valueCase)) {
+      if (extractedString != null || extractedLong != null || extractedDouble 
!= null) {
+        throw new IllegalStateException("Fory returned a value for a missing 
benchmark path");
+      }
+    } else if (!expectedString.equals(extractedString) || ((Number) 
extractedLong).longValue() != expectedLong
+        || Double.compare(((Number) extractedDouble).doubleValue(), 
expectedDouble) != 0) {
+      throw new IllegalStateException("Direct Fory extraction produced an 
unexpected benchmark result");
+    }
+
+    String jaywayString = JsonFunctions.jsonPathString(_json, _path, 
STRING_DEFAULT);
+    String fastString = JsonFunctions.jsonPathStringFast(_json, _path, 
STRING_DEFAULT);
+    String foryString = JsonFunctions.jsonPathStringFory(_json, _path, 
STRING_DEFAULT);
+    if (!expectedString.equals(jaywayString) || 
!jaywayString.equals(fastString) || !jaywayString.equals(foryString)) {
+      throw new IllegalStateException("JSON string extractors disagree for " + 
_path);
+    }
+
+    long jaywayLong = JsonFunctions.jsonPathLong(_json, _longPath, 
LONG_DEFAULT);
+    long fastLong = JsonFunctions.jsonPathLongFast(_json, _longPath, 
LONG_DEFAULT);
+    long foryLong = JsonFunctions.jsonPathLongFory(_json, _longPath, 
LONG_DEFAULT);
+    if (jaywayLong != expectedLong || fastLong != jaywayLong || foryLong != 
jaywayLong) {
+      throw new IllegalStateException("JSON long extractors disagree for " + 
_longPath);
+    }
+
+    double jaywayDouble = JsonFunctions.jsonPathDouble(_json, _doublePath, 
DOUBLE_DEFAULT);
+    double fastDouble = JsonFunctions.jsonPathDoubleFast(_json, _doublePath, 
DOUBLE_DEFAULT);
+    double foryDouble = JsonFunctions.jsonPathDoubleFory(_json, _doublePath, 
DOUBLE_DEFAULT);
+    if (Double.compare(jaywayDouble, expectedDouble) != 0 || 
Double.compare(fastDouble, jaywayDouble) != 0
+        || Double.compare(foryDouble, jaywayDouble) != 0) {
+      throw new IllegalStateException("JSON double extractors disagree for " + 
_doublePath);
+    }
+  }
+
+  private static String buildJson(int targetBytes) {
+    if (BASE_JSON.length() >= targetBytes) {
+      return BASE_JSON;
+    }
+    String marker = "\"trailer\":";
+    int markerOffset = BASE_JSON.indexOf(marker);
+    String paddingPrefix = "\"padding\":\"";
+    String paddingSuffix = "\",";
+    if (BASE_JSON.length() + paddingPrefix.length() + paddingSuffix.length() 
>= targetBytes) {
+      return BASE_JSON;
+    }
+    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);
   }
 
   @Benchmark
   public Object jaywayOneColumn() {
-    return PARSE_CONTEXT.parse(JSON).read(_path, NO_PREDICATES);
+    return PARSE_CONTEXT.parse(_json).read(_path, NO_PREDICATES);
+  }
+
+  /// Fory dynamic-tree parsing plus Jayway traversal, retained as a 
non-production comparison baseline.
+  @Benchmark
+  public Object foryOneColumn() {
+    Object root = FORY_JSON.fromJson(_json, Object.class);
+    return PARSE_CONTEXT.parse(root).read(_path, NO_PREDICATES);
   }
 
   @Benchmark
   public Object fastOneColumnFullScan() {
-    return FastJsonPathExtractor.extract(JSON, _simplePath, false, false);
+    return FastJsonPathExtractor.extract(_json, _simplePath, false, false);
   }
 
   @Benchmark
   public Object fastOneColumnEarlyExit() {
-    return FastJsonPathExtractor.extract(JSON, _simplePath, false, true);
+    return FastJsonPathExtractor.extract(_json, _simplePath, false, true);
   }
 
   /// The existing Jayway scalar function (applicability check + String 
coercion).
   @Benchmark
   public String jsonPathStringJayway() {
-    return JsonFunctions.jsonPathString(JSON, _path, "");
+    return JsonFunctions.jsonPathString(_json, _path, STRING_DEFAULT);
   }
 
-  /// The opt-in fast scalar function, full scan (exact parity).
+  /// The production Fory scalar function (applicability check + String 
coercion).
+  @Benchmark
+  public String jsonPathStringFory() {
+    return JsonFunctions.jsonPathStringFory(_json, _path, STRING_DEFAULT);
+  }
+
+  /// The opt-in fast scalar function, full scan with fallback for unsupported 
or failed extraction.
   @Benchmark
   public String jsonPathStringFast() {
-    return JsonFunctions.jsonPathStringFast(JSON, _path, "");
+    return JsonFunctions.jsonPathStringFast(_json, _path, STRING_DEFAULT);
   }
 
   /// The opt-in fast scalar function, early exit / first match.
   @Benchmark
   public String jsonPathStringFirstMatch() {
-    return JsonFunctions.jsonPathStringFirstMatch(JSON, _path, "");
+    return JsonFunctions.jsonPathStringFirstMatch(_json, _path, 
STRING_DEFAULT);
+  }
+
+  /// The existing Jayway scalar function (applicability check + long 
coercion).
+  @Benchmark
+  public long jsonPathLongJayway() {
+    return JsonFunctions.jsonPathLong(_json, _longPath, LONG_DEFAULT);
+  }
+
+  /// The opt-in fast scalar function, full scan with fallback for unsupported 
or failed extraction.
+  @Benchmark
+  public long jsonPathLongFast() {
+    return JsonFunctions.jsonPathLongFast(_json, _longPath, LONG_DEFAULT);
+  }
+
+  /// The production Fory scalar function (applicability check + long 
coercion).
+  @Benchmark
+  public long jsonPathLongFory() {
+    return JsonFunctions.jsonPathLongFory(_json, _longPath, LONG_DEFAULT);
+  }
+
+  /// The existing Jayway scalar function (applicability check + double 
coercion).
+  @Benchmark
+  public double jsonPathDoubleJayway() {
+    return JsonFunctions.jsonPathDouble(_json, _doublePath, DOUBLE_DEFAULT);
+  }
+
+  /// The opt-in fast scalar function, full scan with fallback for unsupported 
or failed extraction.
+  @Benchmark
+  public double jsonPathDoubleFast() {
+    return JsonFunctions.jsonPathDoubleFast(_json, _doublePath, 
DOUBLE_DEFAULT);
+  }
+
+  /// The production Fory scalar function (applicability check + double 
coercion).
+  @Benchmark
+  public double jsonPathDoubleFory() {
+    return JsonFunctions.jsonPathDoubleFory(_json, _doublePath, 
DOUBLE_DEFAULT);
+  }
+
+  @Benchmark
+  public String jsonPathStringJaywayComplex() {
+    return JsonFunctions.jsonPathString(_json, COMPLEX_PATH, "");
+  }
+
+  @Benchmark
+  public String jsonPathStringFastComplex() {
+    return JsonFunctions.jsonPathStringFast(_json, COMPLEX_PATH, "");
+  }
+
+  @Benchmark
+  public String jsonPathStringForyComplex() {
+    return JsonFunctions.jsonPathStringFory(_json, COMPLEX_PATH, "");
   }
 
   @Benchmark
   public Object jaywayFourColumns() {
     Object last = null;
     for (String path : FOUR_PATHS) {
-      last = PARSE_CONTEXT.parse(JSON).read(path, NO_PREDICATES);
+      last = PARSE_CONTEXT.parse(_json).read(path, NO_PREDICATES);
     }
     return last;
   }
@@ -159,14 +338,36 @@ public class BenchmarkJsonPathExtraction {
   public Object fastFourColumnsSeparatePasses() {
     Object last = null;
     for (SimpleJsonPath path : _simpleFourPaths) {
-      last = FastJsonPathExtractor.extract(JSON, path, false, false);
+      last = FastJsonPathExtractor.extract(_json, path, false, false);
     }
     return last;
   }
 
   @Benchmark
   public Object[] fastFourColumnsSinglePass() {
-    FastJsonPathExtractor.extract(JSON, _simpleFourPaths, _fourResults, false, 
false);
+    FastJsonPathExtractor.extract(_json, _simpleFourPaths, _fourResults, 
false, false);
     return _fourResults;
   }
+
+  /// Models four independent scalar expressions: each call streams over the 
document again.
+  @Benchmark
+  public String foryFourColumnsSeparateParses() {
+    String last = null;
+    for (String path : FOUR_PATHS) {
+      last = JsonFunctions.jsonPathStringFory(_json, path, "");
+    }
+    return last;
+  }
+
+  /// Upper bound for a future parse-sharing optimization; Pinot does not 
currently expose this execution shape.
+  @Benchmark
+  public Object foryFourColumnsSingleParse() {
+    Object root = FORY_JSON.fromJson(_json, Object.class);
+    DocumentContext context = PARSE_CONTEXT.parse(root);
+    Object last = null;
+    for (String path : FOUR_PATHS) {
+      last = context.read(path, NO_PREDICATES);
+    }
+    return last;
+  }
 }
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
index fb278aef2d1..4416e47bb52 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
@@ -146,7 +146,7 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
   /// `SqlNode` in that position: operand checking runs before 
`PinotEvaluateLiteralRule` folds constant
   /// expressions, so an argument such as `CONCAT('$.', 'foo')` folds to a 
literal and plans and executes
   /// correctly. Requiring [org.apache.calcite.sql.type.OperandTypes#LITERAL] 
there would reject these queries,
-  /// which plan and execute successfully on master. Regression guard for all 
three JSON scalar transforms, which
+  /// which plan and execute successfully on master. Regression guard for all 
JSON scalar transforms, which
   /// share one operand checker; 
`QueryRunnerTest#provideTestSqlWithExecutionException` covers the end-to-end 
half,
   /// asserting that a folded path is actually applied on the leaf stage.
   ///
@@ -154,8 +154,8 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
   /// for return-type inference to see it.
   @Test
   public void testJsonExtractScalarAcceptsFoldableJsonPath() {
-    List<String> functions =
-        List.of("JSON_EXTRACT_SCALAR", "JSON_EXTRACT_SCALAR_FAST", 
"JSON_EXTRACT_SCALAR_FIRST_MATCH");
+    List<String> functions = List.of("JSON_EXTRACT_SCALAR", 
"JSON_EXTRACT_SCALAR_FAST",
+        "JSON_EXTRACT_SCALAR_FIRST_MATCH", "JSON_EXTRACT_SCALAR_FORY");
     for (String function : functions) {
       for (String path : List.of("CONCAT('$.', 'foo')", "CAST('$.foo' AS 
VARCHAR)", "UPPER('$.foo')")) {
         String query = "SELECT " + function + "(col1, " + path + ", 'INT') 
FROM a";
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryEnvironmentTestBase.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryEnvironmentTestBase.java
index e26dd6222e2..fa42e993f27 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryEnvironmentTestBase.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryEnvironmentTestBase.java
@@ -258,6 +258,8 @@ public class QueryEnvironmentTestBase {
         new Object[]{"SELECT JSON_EXTRACT_SCALAR_FIRST_MATCH(col1, '$.foo', 
'LONG', '0') FROM a"},
         new Object[]{"SELECT JSON_EXTRACT_SCALAR_FIRST_MATCH(col1, '$.foo', 
'STRING_ARRAY') FROM a"},
         new Object[]{"SELECT JSON_EXTRACT_SCALAR_FIRST_MATCH(col1, '$.foo', 
'TIMESTAMP_ARRAY') FROM a"},
+        new Object[]{"SELECT JSON_EXTRACT_SCALAR_FORY(col1, '$.foo', 'LONG', 
'0') FROM a"},
+        new Object[]{"SELECT JSON_EXTRACT_SCALAR_FORY(col1, '$.foo', 
'DOUBLE_ARRAY') FROM a"},
         new Object[]{"SELECT ts_timestamp FROM a WHERE ts_timestamp BETWEEN 
TIMESTAMP '2016-01-01 00:00:00' AND "
               + "TIMESTAMP '2016-01-01 10:00:00'"},
         new Object[]{"SELECT ts_timestamp FROM a WHERE ts_timestamp >= 
CAST(1454284798000 AS TIMESTAMP)"},
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
index f00dd6413f2..00121f59d66 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
@@ -330,9 +330,9 @@ public class QueryRunnerTest extends QueryRunnerTestBase {
     testCases.add(new Object[]{
         "SELECT CAST(jsonExtractScalar(col1, CONCAT('pa', 'th'), 'INT') AS 
INT) FROM a", "Cannot resolve JSON path"});
     //    - the flip side: a jsonPath that cannot fold to a literal is still 
rejected, on the leaf stage rather than
-    //      during validation. Covers all three variants, which share the 
operand type checker.
+    //      during validation. Covers all four variants, which share the 
operand type checker.
     for (String jsonExtractScalar : new String[]{
-        "jsonExtractScalar", "jsonExtractScalarFast", 
"jsonExtractScalarFirstMatch"
+        "jsonExtractScalar", "jsonExtractScalarFast", 
"jsonExtractScalarFirstMatch", "jsonExtractScalarFory"
     }) {
       testCases.add(new Object[]{
           "SELECT " + jsonExtractScalar + "(col1, col2, 'INT') FROM a",
diff --git a/pom.xml b/pom.xml
index f28203a1964..f249ac32891 100644
--- a/pom.xml
+++ b/pom.xml
@@ -191,6 +191,7 @@
     <helix.version>2.0.1</helix.version>
     <zkclient.version>0.11</zkclient.version>
     <jackson.version>2.22.1</jackson.version>
+    <fory.version>1.6.0</fory.version>
     <zookeeper.version>3.9.5</zookeeper.version>
     <async-http-client.version>3.0.12</async-http-client.version>
     <jersey.version>2.48</jersey.version>
@@ -1432,6 +1433,11 @@
         <artifactId>json-path</artifactId>
         <version>${jsonpath.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.fory</groupId>
+        <artifactId>fory-json</artifactId>
+        <version>${fory.version}</version>
+      </dependency>
       <dependency>
         <groupId>net.minidev</groupId>
         <artifactId>json-smart</artifactId>


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

Reply via email to