Jackie-Jiang commented on code in PR #18979:
URL: https://github.com/apache/pinot/pull/18979#discussion_r4076796596
##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/operands/FunctionOperand.java:
##########
@@ -121,6 +130,19 @@ public Object apply(List<Object> row) {
Object value = operand.apply(row);
_reusableOperandHolder[i] = value != null ?
operand.getResultType().toExternal(value) : null;
}
+ if (_replaceNullJsonOperands) {
+ if (_reusableOperandHolder[0] == null) {
+ ColumnDataType inputType = _operands.get(0).getResultType();
+ Object nullPlaceholder = inputType.getNullPlaceholder();
+ // An untyped SQL NULL has UNKNOWN type and therefore no generic
placeholder. The leaf transform reads every
+ // non-BYTES JSON input through transformToStringValuesSV(), whose
NULL literal value is the empty string.
+ _reusableOperandHolder[0] = nullPlaceholder != null ?
inputType.toExternal(nullPlaceholder) : "";
+ }
+ if (_reusableOperandHolder.length == 4 && _reusableOperandHolder[3] ==
null
+ && _reusableOperandHolder[2] != null) {
+ _reusableOperandHolder[3] =
getJsonNullDefault(_reusableOperandHolder[2].toString());
Review Comment:
Optional simplification: precompute the NULL default
The result-type argument and explicit default are validated literals, so the
replacement for a NULL default can be prepared once in the constructor. Calling
`getJsonNullDefault()` here repeats uppercasing, `_ARRAY` suffix handling, type
dispatch, and the empty byte-array allocation for every affected row. Caching
the prepared replacement would simplify this evaluation path while preserving
the null-handling behavior.
##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ArrayLiteralTransformFunction.java:
##########
@@ -144,6 +144,17 @@ public
ArrayLiteralTransformFunction(List<ExpressionContext> literalContexts) {
}
_dataType = literalContexts.get(0).getLiteral().getType();
switch (_dataType) {
+ case BOOLEAN:
Review Comment:
[P2] Complete conversions for the new BOOLEAN array literal
This branch stores Boolean values in `_intArrayLiteral` while leaving
`_dataType` as BOOLEAN, but the long/float/double/string conversion switches
still lack BOOLEAN handling. For example, `SELECT
arraySum(jsonExtractScalar('{"v":[true,false]}', '$.v', 'BOOLEAN_ARRAY')) FROM
a` folds the inner expression to an ARRAY call and leaves the outer function
for runtime evaluation. At the leaf, `ArraySumTransformFunction` accepts
BOOLEAN through its numeric stored INT type, then calls
`transformToDoubleValuesMV()`, which throws `Unable to convert data type:
BOOLEAN to double array`. Please complete the conversions for this
representation and cover a composed consumer such as arraySum; the direct
int-array assertion does not exercise this path.
##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotEvaluateLiteralRule.java:
##########
@@ -88,13 +91,15 @@ private static LogicalProject
constructNewProject(LogicalProject oldProject, Log
List<RexNode> castedNewProjects = new ArrayList<>(numProjects);
boolean needCast = false;
for (int i = 0; i < numProjects; i++) {
- RexNode oldNode = oldProjects.get(i);
RexNode newNode = newProjects.get(i);
+ RelDataType expectedType =
oldProject.getRowType().getFieldList().get(i).getType();
// Need to cast the result to the original type if the literal type is
changed, e.g. VARCHAR literal is typed as
- // CHAR(STRING_LENGTH) in Calcite, but we need to cast it back to
VARCHAR.
- if (!oldNode.getType().equals(newNode.getType())) {
+ // CHAR(STRING_LENGTH) in Calcite, but we need to cast it back to
VARCHAR. Use the project's declared row type
+ // instead of the original expression type because a nullable
user-defined function can retain a non-null operand
+ // type while the validated projection is nullable.
+ if (!expectedType.equals(newNode.getType())) {
needCast = true;
- newNode = rexBuilder.makeCast(oldNode.getType(), newNode, true);
+ newNode = rexBuilder.makeAbstractCast(expectedType, newNode, false);
Review Comment:
[P2] Narrow the projection cast change to the required type-restoration case
Replacing `makeCast` with `makeAbstractCast` here affects every restored
projection type, including unrelated folded string and spatial functions. It
leaves explicit CAST nodes around `substr('month', 2)`, `concat('month', '
1')`, nested upper/lower/substr, and `ST_Point(20, 10, 1)`. These are the four
failing `literal_evaluation_tests` cases in [Unit Test Set
1](https://github.com/apache/pinot/actions/runs/35414159549/job/105819375038),
which also prevents the downstream query-runtime tests from running. Please
preserve ordinary literal normalization and narrowly handle the
JSON/nullability case that needs different restoration. The demonstrated
regression here is plan churn and failing planner tests; I have not established
incorrect query results.
##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotEvaluateLiteralRule.java:
##########
@@ -322,4 +354,38 @@ private static Object convertResultValue(@Nullable Object
resultValue, RelDataTy
// TODO: Add more type handling
return resultValue;
}
+
+ /// Boxes a Java array so the folder can build a Calcite array literal.
`jsonExtractScalar`
Review Comment:
Optional simplification: build the converted array values in one pass
This planner-time helper duplicates the same boxing loop for Object[],
int[], long[], float[], double[], and boolean[], and its caller immediately
copies the resulting list into another list while converting each element. A
single loop using `java.lang.reflect.Array.getLength()` and `Array.get()` can
handle the supported primitive/object arrays, call `convertResultValue()`
directly, and produce the final list. That would remove the type branches and
intermediate collection without removing support for primitive-array results.
##########
pinot-common/src/main/java/org/apache/pinot/common/utils/JsonNumberUtils.java:
##########
@@ -0,0 +1,237 @@
+/**
+ * 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.utils;
+
+import java.math.BigDecimal;
+
+
+/// Shared JSON-number parser used by `jsonExtractScalar` (scalar and
transform).
+///
+/// Accepts regular long syntax plus JSON numeric forms: `1E1` → `10`, `1.9` →
`1` (truncate toward
+/// zero), `1.123E1` → `11`. Dotted-exponent values are parsed as an exact
decimal ([BigDecimal]),
+/// truncated toward zero, then range-checked, so `9.223372036854775807E18` is
[Long#MAX_VALUE] and
+/// `9007199254740993.0E0` keeps the bit that a `double` would drop. Throws
[NumberFormatException]
+/// with `For input string: "<value>"` on overflow (`9223372036854775808`,
`2.0E19`), illegal
+/// exponent (`2E20`, `2E-1`), and other malformed input.
+///
+/// Thread-safe: no mutable state.
+public final class JsonNumberUtils {
+ private static final BigDecimal LONG_MIN_MINUS_ONE = new
BigDecimal("-9223372036854775809");
+ private static final BigDecimal LONG_MAX_PLUS_ONE = new
BigDecimal("9223372036854775808");
+
+ private static final long[] POWERS_OF_10 = new long[]{
+ 1L,
+ 10L,
+ 100L,
+ 1000L,
+ 10000L,
+ 100000L,
+ 1000000L,
+ 10000000L,
+ 100000000L,
+ 1000000000L,
+ 10000000000L,
+ 100000000000L,
+ 1000000000000L,
+ 10000000000000L,
+ 100000000000000L,
+ 1000000000000000L,
+ 10000000000000000L,
+ 100000000000000000L,
+ 1000000000000000000L,
+ };
+
+ private JsonNumberUtils() {
+ }
+
+ /// Parses a JSON numeric string to a long.
+ ///
+ /// @param cs char sequence to parse
+ /// @return parsed long value
+ /// @throws NumberFormatException if `cs` is null, empty, out of long range,
or not a JSON number
+ public static long parseJsonLong(CharSequence cs) {
+ if (cs == null) {
+ throw new NumberFormatException("Can't parse null string");
+ }
+
+ boolean negative = false;
+ int i = 0;
+ int len = cs.length();
+ long limit = -Long.MAX_VALUE;
+
+ if (len <= 0) {
+ throw formatException(cs);
+ }
+
+ boolean dotFound = false;
+ boolean exponentFound = false;
+
+ char firstChar = cs.charAt(0);
+ if (firstChar < '0') { // Possible leading "+" or "-"
+ if (firstChar == '-') {
+ negative = true;
+ limit = Long.MIN_VALUE;
+ } else if (firstChar != '+') {
+ throw formatException(cs);
+ }
+
+ if (len == 1) { // Cannot have lone "+" or "-"
+ throw formatException(cs);
+ }
+ i++;
+ }
+ long multmin = limit / 10;
+ long result = 0;
+ while (i < len) {
+ // Accumulating negatively avoids surprises near MAX_VALUE
+ char c = cs.charAt(i++);
+ if (c < '0' || c > '9' || result < multmin) {
+ if (c == '.') {
+ // ignore the rest of the integer digits
+ dotFound = true;
+ break;
+ } else if (c == 'e' || c == 'E') {
+ exponentFound = true;
+ break;
+ }
+ throw formatException(cs);
+ }
+
+ int digit = c - '0';
+ result *= 10;
+ if (result < limit + digit) {
+ throw formatException(cs);
+ }
+ result -= digit;
+ }
+
+ if (dotFound) {
+ // scan rest of the string to make sure it's only digits (or an exponent)
+ while (i < len) {
+ char c = cs.charAt(i++);
+ if (c < '0' || c > '9') {
+ if ((c | 32) == 'e') {
+ exponentFound = true;
+ break;
+ } else {
+ throw formatException(cs);
+ }
+ }
+ }
+ }
+
+ if (exponentFound) {
+ if (dotFound) {
+ // Do not go through double: 9.223372036854775807E18 is
Long.MAX_VALUE, but
+ // Double.parseDouble rounds it to 2^63, which is out of range.
+ return parseExactLong(cs);
+ }
+
+ long exp;
+ try {
+ exp = parseWholeLong(cs, i, len);
+ } catch (NumberFormatException nfe) {
+ throw new NumberFormatException("Wrong exponent");
+ }
+
+ if (exp < 0 || exp >= POWERS_OF_10.length) {
+ throw new NumberFormatException("Wrong exponent");
+ }
+
+ try {
+ return Math.multiplyExact(negative ? result : -result,
POWERS_OF_10[(int) exp]);
+ } catch (ArithmeticException e) {
+ throw formatException(cs);
+ }
+ }
+
+ return negative ? result : -result;
+ }
+
+ /// Parses a JSON numeric string as an exact decimal, truncates toward zero,
then requires a long.
+ private static long parseExactLong(CharSequence cs) {
+ try {
+ return truncateToLong(new BigDecimal(cs.toString()));
+ } catch (NumberFormatException e) {
+ throw formatException(cs);
+ }
+ }
+
+ /// Truncates an exact decimal toward zero and requires the result to fit in
a long.
+ ///
+ /// The comparisons happen before conversion so a value such as
`1E999999999` is rejected
+ /// without expanding its exponent into an enormous integer. Values strictly
+ /// between `Long.MIN_VALUE - 1` and `Long.MAX_VALUE + 1` truncate into the
long range.
+ public static long truncateToLong(BigDecimal value) {
+ if (value.compareTo(LONG_MIN_MINUS_ONE) <= 0 ||
value.compareTo(LONG_MAX_PLUS_ONE) >= 0) {
+ throw formatException(value.toString());
+ }
+ return value.longValue();
+ }
+
+ /// Parses `cs[start, end)` as a whole long (sign allowed). Used for the
exponent field.
+ private static long parseWholeLong(CharSequence cs, int start, int end) {
Review Comment:
Optional simplification: reuse the JDK parser for the exponent
`parseWholeLong()` is a second signed-long parser used only for the exponent
substring. An ASCII-digit validation pass (allowing the existing leading sign),
followed by `Long.parseLong(cs, start, end, 10)`, can replace the manual
accumulation and overflow checks. Keep the ASCII validation because the JDK
accepts Unicode digits, and retain the caller's `Wrong exponent` exception
translation so behavior stays consistent.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]