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


##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotEvaluateLiteralRule.java:
##########
@@ -251,6 +264,112 @@ private static RexNode 
evaluateLiteralOnlyFunction(RexCall rexCall, RexBuilder r
     }
   }
 
+  /// Returns whether the call is `datetime + interval` (Calcite's 
`DATETIME_PLUS`) or `datetime - interval`
+  /// (`MINUS_DATE`) with a `TIMESTAMP` or `DATE` result. Calcite puts the 
interval second for both.
+  private static boolean isDatetimeIntervalArithmetic(RexCall rexCall) {
+    SqlKind kind = rexCall.getKind();
+    SqlTypeName resultType = rexCall.getType().getSqlTypeName();
+    List<RexNode> operands = rexCall.getOperands();
+    return (kind == SqlKind.PLUS || kind == SqlKind.MINUS) && (resultType == 
SqlTypeName.TIMESTAMP
+        || resultType == SqlTypeName.DATE) && operands.size() == 2 && 
SqlTypeUtil.isInterval(operands.get(1).getType());
+  }
+
+  /// Evaluates `datetime + interval` or `datetime - interval` with the same 
semantics as Calcite. It cannot use Pinot's
+  /// `plus` / `minus` functions, because Calcite stores a day-time interval 
as milliseconds but a year-month interval
+  /// as months: they would add `INTERVAL '1' MONTH` as 1 millisecond. Months 
are added with the same calendar
+  /// arithmetic as `TIMESTAMPADD(MONTH, ...)`, so `2019-01-31 + INTERVAL '1' 
MONTH` is `2019-02-28`. Like Calcite, only
+  /// the whole days of a day-time interval are added to a `DATE`.
+  private static RexNode evaluateDatetimeIntervalArithmetic(RexCall rexCall, 
RexBuilder rexBuilder) {
+    RexLiteral datetime = (RexLiteral) rexCall.getOperands().get(0);
+    RexLiteral interval = (RexLiteral) rexCall.getOperands().get(1);
+    RelDataType resultType = rexCall.getType();
+    if (datetime.isNull() || interval.isNull()) {
+      return rexBuilder.makeNullLiteral(resultType);
+    }
+    Object resultValue;
+    try {
+      long millis = 
Objects.requireNonNull(datetime.getValueAs(Calendar.class)).getTimeInMillis();
+      // Number of months for a year-month interval, number of milliseconds 
for a day-time interval
+      long intervalValue = 
Objects.requireNonNull(interval.getValueAs(BigDecimal.class)).longValueExact();
+      if (rexCall.getKind() == SqlKind.MINUS) {
+        intervalValue = Math.negateExact(intervalValue);
+      }
+      boolean isDate = resultType.getSqlTypeName() == SqlTypeName.DATE;
+      long resultMillis;
+      if 
(SqlTypeName.YEAR_INTERVAL_TYPES.contains(interval.getType().getSqlTypeName())) 
{
+        resultMillis = DateTimeFunctions.timestampAdd("MONTH", intervalValue, 
millis);
+      } else {
+        if (isDate) {
+          intervalValue = intervalValue / DateTimeUtils.MILLIS_PER_DAY * 
DateTimeUtils.MILLIS_PER_DAY;
+        }
+        resultMillis = Math.addExact(millis, intervalValue);
+      }
+      resultValue = isDate ? DateString.fromDaysSinceEpoch(
+          Math.toIntExact(Math.floorDiv(resultMillis, 
DateTimeUtils.MILLIS_PER_DAY))) : resultMillis;
+    } catch (Exception e) {
+      throw new SqlCompilationException(
+          "Caught exception while evaluating: " + rexCall + ": " + 
e.getMessage(), e);
+    }
+    return rexBuilder.makeLiteral(resultValue, resultType, false);
+  }
+
+  /// Returns whether the call is `+`, `-`, `*` or `/` with an interval result 
and only interval or numeric operands,
+  /// e.g. `INTERVAL '1' MONTH * 2`.
+  private static boolean isIntervalArithmetic(RexCall rexCall) {
+    SqlKind kind = rexCall.getKind();
+    List<RexNode> operands = rexCall.getOperands();
+    return (kind == SqlKind.PLUS || kind == SqlKind.MINUS || kind == 
SqlKind.TIMES || kind == SqlKind.DIVIDE)
+        && SqlTypeUtil.isInterval(rexCall.getType()) && operands.size() == 2 
&& operands.stream()
+        .allMatch(operand -> SqlTypeUtil.isInterval(operand.getType()) || 
SqlTypeUtil.isNumeric(operand.getType()));
+  }
+
+  /// Evaluates interval arithmetic exactly. Pinot's `times` / `divide` 
functions compute in DOUBLE, so
+  /// `INTERVAL '100' MONTH * 0.29` would be 28.999999999999996 months instead 
of 29, and `INTERVAL '1' DAY * 0.7` would
+  /// be 1 millisecond short.
+  private static RexNode evaluateIntervalArithmetic(RexCall rexCall, 
RexBuilder rexBuilder) {
+    RexLiteral left = (RexLiteral) rexCall.getOperands().get(0);
+    RexLiteral right = (RexLiteral) rexCall.getOperands().get(1);
+    RelDataType resultType = rexCall.getType();
+    if (left.isNull() || right.isNull()) {
+      return rexBuilder.makeNullLiteral(resultType);
+    }
+    BigDecimal resultValue;
+    try {
+      // An interval literal holds its number of months or milliseconds
+      BigDecimal leftValue = 
Objects.requireNonNull(left.getValueAs(BigDecimal.class));
+      BigDecimal rightValue = 
Objects.requireNonNull(right.getValueAs(BigDecimal.class));
+      switch (rexCall.getKind()) {
+        case PLUS:
+          resultValue = leftValue.add(rightValue);
+          break;
+        case MINUS:
+          resultValue = leftValue.subtract(rightValue);
+          break;
+        case TIMES:
+          resultValue = leftValue.multiply(rightValue);
+          break;
+        default:
+          resultValue = leftValue.divide(rightValue, MathContext.DECIMAL128);
+          break;
+      }
+      resultValue = toIntervalValue(resultValue, resultType);
+    } catch (Exception e) {
+      throw new SqlCompilationException(
+          "Caught exception while evaluating: " + rexCall + ": " + 
e.getMessage(), e);
+    }
+    return rexBuilder.makeLiteral(resultValue, resultType, false);
+  }
+
+  /// Converts a number to the value that Calcite stores for an interval: a 
whole number of months for a year-month
+  /// interval, and milliseconds for a day-time interval (like Calcite, 
fractions of a millisecond are dropped).
+  private static BigDecimal toIntervalValue(BigDecimal value, RelDataType 
intervalType) {
+    if 
(SqlTypeName.YEAR_INTERVAL_TYPES.contains(intervalType.getSqlTypeName())) {

Review Comment:
   Minor / fail-safe divergence from Calcite runtime, not blocking. Calcite 
stores a year-month interval as an integer month count, so `INTERVAL '1' MONTH 
/ 2` silently truncates to `INTERVAL '0' MONTH` at runtime rather than 
erroring. Here the fold throws instead. That's arguably better (hard error vs 
silent 0), but it does mean the folded form rejects a query the runtime path 
would accept. Consider either truncating with `RoundingMode.DOWN` to match 
Calcite, or adding a short comment noting this is an intentional 
stricter-than-Calcite rejection.



##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotEvaluateLiteralRule.java:
##########
@@ -310,6 +429,13 @@ private static Object convertResultValue(@Nullable Object 
resultValue, RelDataTy
     if (relDataType.getSqlTypeName() == SqlTypeName.DECIMAL) {
       return new BigDecimal(resultValue.toString());
     }
+    // Other functions with an interval result, e.g. ABS(INTERVAL '-1' MONTH). 
Interval arithmetic is evaluated exactly
+    // in evaluateIntervalArithmetic().
+    if (SqlTypeUtil.isInterval(relDataType)) {
+      double value = ((Number) resultValue).doubleValue();

Review Comment:
   Minor: this generic interval branch routes the value through `double` before 
`toIntervalValue`, which gives up the exactness the arithmetic path works hard 
to preserve with BigDecimal. In practice the only callers here (e.g. 
`ABS(INTERVAL ...)`) return exact integers so it's fine, but if a `Number` 
subtype that's already a `BigDecimal` reaches this, converting via 
`doubleValue()` is a small, avoidable precision hazard — you could branch on 
`resultValue instanceof BigDecimal` and skip the double.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to