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

Jackie-Jiang 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 dc95530c8d5 Convert non-JSON-native scalars in jsonPathLong and 
jsonPathDouble (#19138)
dc95530c8d5 is described below

commit dc95530c8d5f7682eb889d2b63c4cad8697a6a9d
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Jul 31 18:11:48 2026 -0700

    Convert non-JSON-native scalars in jsonPathLong and jsonPathDouble (#19138)
---
 .../common/function/scalar/JsonFunctions.java      | 86 +++++++++++++++-------
 .../pinot/common/function/JsonFunctionsTest.java   | 44 +++++++++++
 2 files changed, 102 insertions(+), 28 deletions(-)

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 58f270788c9..df409185e4b 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
@@ -31,6 +31,7 @@ import com.jayway.jsonpath.spi.cache.CacheProvider;
 import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
 import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.ArrayList;
@@ -291,18 +292,39 @@ public class JsonFunctions {
     }
     try {
       Object jsonValue = jsonPath(object, jsonPath);
-      if (jsonValue == null) {
-        return defaultValue;
-      }
-      if (jsonValue instanceof Number) {
-        return ((Number) jsonValue).longValue();
-      }
-      return Long.parseLong(jsonValue.toString());
+      return jsonValue != null ? jsonValueToLong(jsonValue) : defaultValue;
     } catch (Exception ignore) {
       return defaultValue;
     }
   }
 
+  /// Converts a value resolved by a JSON path to `long` for the 
`jsonPathLong*` family, following the
+  /// conversion conventions of [org.apache.pinot.spi.utils.PinotDataType]. A 
`Number` yields its `longValue`
+  /// and a `Boolean` yields `1` / `0`, matching `jsonExtractScalar` on every 
value a parsed JSON document can
+  /// produce. [Timestamp] / [LocalDate] / [LocalTime] are non-JSON-native 
scalars a record extractor can
+  /// materialize; they have no parseable numeric `toString`, so they convert 
to their Pinot internal numeric
+  /// form - epoch millis, days since epoch, and millis since midnight 
respectively. Anything else (e.g. a JSON
+  /// string holding a number) is parsed from its `toString`, which throws for 
a value with no numeric meaning
+  /// (`UUID`, `byte[]`, a container) and leaves the caller returning its 
default value.
+  private static long jsonValueToLong(Object jsonValue) {
+    if (jsonValue instanceof Number) {
+      return ((Number) jsonValue).longValue();
+    }
+    if (jsonValue instanceof Boolean) {
+      return (Boolean) jsonValue ? 1L : 0L;
+    }
+    if (jsonValue instanceof Timestamp) {
+      return ((Timestamp) jsonValue).getTime();
+    }
+    if (jsonValue instanceof LocalDate) {
+      return ((LocalDate) jsonValue).toEpochDay();
+    }
+    if (jsonValue instanceof LocalTime) {
+      return ((LocalTime) jsonValue).toNanoOfDay() / 1_000_000L;
+    }
+    return Long.parseLong(jsonValue.toString());
+  }
+
   /// Opt-in fast variant of [#jsonPathLong(Object, String, long)] with 
identical results; see
   /// [#jsonPathStringFast] for the fast/Jayway-fallback contract.
   @ScalarFunction(nullableParameters = true)
@@ -324,13 +346,7 @@ public class JsonFunctions {
     }
     try {
       Object jsonValue = fastJsonPath(object, jsonPath, false, earlyExit);
-      if (jsonValue == null) {
-        return defaultValue;
-      }
-      if (jsonValue instanceof Number) {
-        return ((Number) jsonValue).longValue();
-      }
-      return Long.parseLong(jsonValue.toString());
+      return jsonValue != null ? jsonValueToLong(jsonValue) : defaultValue;
     } catch (Exception ignore) {
       return defaultValue;
     }
@@ -350,18 +366,38 @@ public class JsonFunctions {
     }
     try {
       Object jsonValue = jsonPath(object, jsonPath);
-      if (jsonValue == null) {
-        return defaultValue;
-      }
-      if (jsonValue instanceof Number) {
-        return ((Number) jsonValue).doubleValue();
-      }
-      return Double.parseDouble(jsonValue.toString());
+      return jsonValue != null ? jsonValueToDouble(jsonValue) : defaultValue;
     } catch (Exception ignore) {
       return defaultValue;
     }
   }
 
+  /// Double counterpart of [#jsonValueToLong], with the same type dispatch: a 
`Number` yields its
+  /// `doubleValue`, a `Boolean` yields `1` / `0`, [Timestamp] / [LocalDate] / 
[LocalTime] yield their internal
+  /// numeric form, and anything else is parsed from its `toString`.
+  ///
+  /// The three temporal forms stay integral here, matching `PinotDataType`: 
sub-millisecond nanos are truncated
+  /// rather than carried as a fraction, so a value extracted into a `DOUBLE` 
column holds exactly what the
+  /// corresponding `TIMESTAMP` / `DATE` / `TIME` column would store.
+  private static double jsonValueToDouble(Object jsonValue) {
+    if (jsonValue instanceof Number) {
+      return ((Number) jsonValue).doubleValue();
+    }
+    if (jsonValue instanceof Boolean) {
+      return (Boolean) jsonValue ? 1d : 0d;
+    }
+    if (jsonValue instanceof Timestamp) {
+      return ((Timestamp) jsonValue).getTime();
+    }
+    if (jsonValue instanceof LocalDate) {
+      return ((LocalDate) jsonValue).toEpochDay();
+    }
+    if (jsonValue instanceof LocalTime) {
+      return ((LocalTime) jsonValue).toNanoOfDay() / 1_000_000L;
+    }
+    return Double.parseDouble(jsonValue.toString());
+  }
+
   /// Opt-in fast variant of [#jsonPathDouble(Object, String, double)] with 
identical results; see
   /// [#jsonPathStringFast] for the fast/Jayway-fallback contract.
   @ScalarFunction(nullableParameters = true)
@@ -383,13 +419,7 @@ public class JsonFunctions {
     }
     try {
       Object jsonValue = fastJsonPath(object, jsonPath, false, earlyExit);
-      if (jsonValue == null) {
-        return defaultValue;
-      }
-      if (jsonValue instanceof Number) {
-        return ((Number) jsonValue).doubleValue();
-      }
-      return Double.parseDouble(jsonValue.toString());
+      return jsonValue != null ? jsonValueToDouble(jsonValue) : defaultValue;
     } catch (Exception ignore) {
       return defaultValue;
     }
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
index 160ebae59bd..58fd8bee4bc 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
@@ -491,6 +491,50 @@ public class JsonFunctionsTest {
     assertEquals(JsonFunctions.jsonPathString(Map.of("v", timestamp), "$.v"), 
JsonUtils.objectToString(timestamp));
   }
 
+  @Test
+  public void testJsonPathLongOnExtractedValues() {
+    // Boolean is JSON-native and follows the numeric convention of 
jsonExtractScalar: true -> 1, false -> 0.
+    assertEquals(JsonFunctions.jsonPathLong("{\"v\":true}", "$.v"), 1L);
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", false), "$.v"), 0L);
+
+    // A value resolved from an already-parsed record tree (not a JSON string) 
keeps its runtime Java type.
+    // Timestamp / LocalDate / LocalTime are non-JSON-native scalars a record 
extractor can materialize; they
+    // convert to their Pinot internal numeric form - epoch millis, days since 
epoch, millis since midnight.
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", new Timestamp(1000L)), 
"$.v"), 1000L);
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", LocalDate.of(2026, 7, 
20)), "$.v"), 20654L);
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", LocalTime.of(19, 54, 
37)), "$.v"), 71677000L);
+
+    // The fast-path variants share the same conversion helper and must 
produce identical results.
+    assertEquals(JsonFunctions.jsonPathLongFast(Map.of("v", new 
Timestamp(1000L)), "$.v", -1L), 1000L);
+    assertEquals(JsonFunctions.jsonPathLongFirstMatch(Map.of("v", true), 
"$.v", -1L), 1L);
+
+    // Every Number narrows through longValue, and a value with no numeric 
meaning yields the default.
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", new 
BigDecimal("123.45")), "$.v"), 123L);
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", UUID.randomUUID()), 
"$.v", -1L), -1L);
+    assertEquals(JsonFunctions.jsonPathLong(Map.of("v", Map.of("k", "w")), 
"$.v", -1L), -1L);
+  }
+
+  @Test
+  public void testJsonPathDoubleOnExtractedValues() {
+    // Boolean is JSON-native and follows the numeric convention of 
jsonExtractScalar: true -> 1, false -> 0.
+    assertEquals(JsonFunctions.jsonPathDouble("{\"v\":true}", "$.v"), 1d);
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", false), "$.v"), 0d);
+
+    // Timestamp / LocalDate / LocalTime convert to the same internal numeric 
form as jsonPathLong.
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", new 
Timestamp(1000L)), "$.v"), 1000d);
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", LocalDate.of(2026, 
7, 20)), "$.v"), 20654d);
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", LocalTime.of(19, 54, 
37)), "$.v"), 71677000d);
+
+    // The fast-path variants share the same conversion helper and must 
produce identical results.
+    assertEquals(JsonFunctions.jsonPathDoubleFast(Map.of("v", new 
Timestamp(1000L)), "$.v", -1d), 1000d);
+    assertEquals(JsonFunctions.jsonPathDoubleFirstMatch(Map.of("v", true), 
"$.v", -1d), 1d);
+
+    // Every Number widens through doubleValue, and a value with no numeric 
meaning yields the default.
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", new 
BigDecimal("123.45")), "$.v"), 123.45d);
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", UUID.randomUUID()), 
"$.v", -1d), -1d);
+    assertEquals(JsonFunctions.jsonPathDouble(Map.of("v", Map.of("k", "w")), 
"$.v", -1d), -1d);
+  }
+
   @DataProvider
   public static Object[][] jsonPathArrayTestCases() {
     return new Object[][]{


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

Reply via email to