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]