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

FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 7e300b9ccf6 fix: Proper handling of numeric CAST to INTERVAL. (#20754)
7e300b9ccf6 is described below

commit 7e300b9ccf6cca26d7bbe632f790415b31f123c1
Author: Gian Merlino <[email protected]>
AuthorDate: Tue Nov 3 18:10:20 2026 -0800

    fix: Proper handling of numeric CAST to INTERVAL. (#20754)
    
    This patch fixes handling of numeric types to INTERVAL types.
    Previously, the unit was not respected, and expressions would yield
    planner errors.
---
 .../expression/builtin/CastOperatorConversion.java |  39 +++++++
 .../apache/druid/sql/calcite/CalciteQueryTest.java | 119 +++++++++++++++++++++
 2 files changed, 158 insertions(+)

diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CastOperatorConversion.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CastOperatorConversion.java
index d82d3ea9546..c1425101dae 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CastOperatorConversion.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CastOperatorConversion.java
@@ -19,9 +19,12 @@
 
 package org.apache.druid.sql.calcite.expression.builtin;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
@@ -39,6 +42,7 @@ import org.apache.druid.sql.calcite.planner.Calcites;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.joda.time.Period;
 
+import java.math.BigDecimal;
 import java.util.function.Function;
 
 public class CastOperatorConversion implements SqlOperatorConversion
@@ -79,6 +83,12 @@ public class CastOperatorConversion implements 
SqlOperatorConversion
       );
     } else if (SqlTypeName.DATETIME_TYPES.contains(fromType) && 
SqlTypeName.CHAR_TYPES.contains(toType)) {
       return castDateTimeToChar(plannerContext, operandExpression, fromType, 
Calcites.getColumnTypeForRelDataType(rexNode.getType()));
+    } else if (SqlTypeName.NUMERIC_TYPES.contains(fromType) && 
SqlTypeName.INTERVAL_TYPES.contains(toType)) {
+      return castNumericToInterval(
+          operandExpression,
+          Calcites.getColumnTypeForRelDataType(operand.getType()),
+          rexNode.getType()
+      );
     } else {
       // Handle other casts. If either type is ANY, use the other type 
instead. If both are ANY, this means nulls
       // downstream, Druid will try its best
@@ -166,6 +176,35 @@ public class CastOperatorConversion implements 
SqlOperatorConversion
     }
   }
 
+  /**
+   * Returns an expression that scales a number to the representation of an 
interval: milliseconds for day-time
+   * intervals and months for year-month intervals.
+   */
+  private static DruidExpression castNumericToInterval(
+      final DruidExpression operand,
+      final ColumnType fromDruidType,
+      final RelDataType toType
+  )
+  {
+    final SqlIntervalQualifier intervalQualifier =
+        Preconditions.checkNotNull(toType.getIntervalQualifier(), 
"intervalQualifier");
+    final BigDecimal multiplier = intervalQualifier.getUnit().multiplier;
+
+    return DruidExpression.ofExpression(
+        ColumnType.LONG,
+        args -> {
+          final String scaled;
+          if (multiplier.compareTo(BigDecimal.ONE) != 0) {
+            scaled = StringUtils.format("(%s * %s)", 
args.getFirst().getExpression(), multiplier.toPlainString());
+          } else {
+            scaled = args.getFirst().getExpression();
+          }
+          return ColumnType.LONG.equals(fromDruidType) ? scaled : 
StringUtils.format("CAST(%s, 'LONG')", scaled);
+        },
+        ImmutableList.of(operand)
+    );
+  }
+
   private static DruidExpression castDateTimeToChar(
       final PlannerContext plannerContext,
       final DruidExpression operand,
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 961e4b08c89..e69f8e9c463 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -5128,6 +5128,125 @@ public class CalciteQueryTest extends 
BaseCalciteQueryTest
     );
   }
 
+  @Test
+  public void testInfixCastToInterval()
+  {
+    // Numbers cast to intervals are in units of the interval's field.
+    testQuery(
+        "SELECT __time + CAST(cnt AS INTERVAL DAY), __time + cnt::INTERVAL 
HOUR, __time + (cnt * 2)::INTERVAL MONTH,"
+        + " __time - m1::BIGINT::INTERVAL SECOND, __time + (cnt - 3)::INTERVAL 
YEAR FROM foo",
+        ImmutableList.of(
+            newScanQueryBuilder()
+                .dataSource(CalciteTests.DATASOURCE1)
+                .intervals(querySegmentSpec(Filtration.eternity()))
+                .virtualColumns(
+                    expressionVirtualColumn("v0", "(\"__time\" + (\"cnt\" * 
86400000))", ColumnType.LONG),
+                    expressionVirtualColumn("v1", "(\"__time\" + (\"cnt\" * 
3600000))", ColumnType.LONG),
+                    expressionVirtualColumn(
+                        "v2",
+                        "timestamp_shift(\"__time\",concat('P', (\"cnt\" * 2), 
'M'),1,'UTC')",
+                        ColumnType.LONG
+                    ),
+                    expressionVirtualColumn("v3", "(\"__time\" - (CAST(\"m1\", 
'LONG') * 1000))", ColumnType.LONG),
+                    expressionVirtualColumn(
+                        "v4",
+                        "timestamp_shift(\"__time\",concat('P', ((\"cnt\" - 3) 
* 12), 'M'),1,'UTC')",
+                        ColumnType.LONG
+                    )
+                )
+                .columns("v0", "v1", "v2", "v3", "v4")
+                .columnTypes(ColumnType.LONG, ColumnType.LONG, 
ColumnType.LONG, ColumnType.LONG, ColumnType.LONG)
+                
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+                .context(QUERY_CONTEXT_DEFAULT)
+                .build()
+        ),
+        ImmutableList.of(
+            new Object[]{
+                timestamp("2000-01-02"),
+                timestamp("2000-01-01T01"),
+                timestamp("2000-03-01"),
+                timestamp("1999-12-31T23:59:59"),
+                timestamp("1998-01-01")
+            },
+            new Object[]{
+                timestamp("2000-01-03"),
+                timestamp("2000-01-02T01"),
+                timestamp("2000-03-02"),
+                timestamp("2000-01-01T23:59:58"),
+                timestamp("1998-01-02")
+            },
+            new Object[]{
+                timestamp("2000-01-04"),
+                timestamp("2000-01-03T01"),
+                timestamp("2000-03-03"),
+                timestamp("2000-01-02T23:59:57"),
+                timestamp("1998-01-03")
+            },
+            new Object[]{
+                timestamp("2001-01-02"),
+                timestamp("2001-01-01T01"),
+                timestamp("2001-03-01"),
+                timestamp("2000-12-31T23:59:56"),
+                timestamp("1999-01-01")
+            },
+            new Object[]{
+                timestamp("2001-01-03"),
+                timestamp("2001-01-02T01"),
+                timestamp("2001-03-02"),
+                timestamp("2001-01-01T23:59:55"),
+                timestamp("1999-01-02")
+            },
+            new Object[]{
+                timestamp("2001-01-04"),
+                timestamp("2001-01-03T01"),
+                timestamp("2001-03-03"),
+                timestamp("2001-01-02T23:59:54"),
+                timestamp("1999-01-03")
+            }
+        )
+    );
+  }
+
+  @Test
+  public void testInfixCastDecimalToInterval()
+  {
+    // Fractions are kept down to the millisecond for day-time intervals, 
rather than truncated to the field.
+    testQuery(
+        "SELECT __time + (m1 / 2)::DECIMAL(5, 1)::INTERVAL DAY, __time + (m1 - 
3.5)::DECIMAL(5, 1)::INTERVAL SECOND"
+        + " FROM foo",
+        ImmutableList.of(
+            newScanQueryBuilder()
+                .dataSource(CalciteTests.DATASOURCE1)
+                .intervals(querySegmentSpec(Filtration.eternity()))
+                .virtualColumns(
+                    expressionVirtualColumn(
+                        "v0",
+                        "(\"__time\" + CAST(((\"m1\" / 2) * 86400000), 
'LONG'))",
+                        ColumnType.LONG
+                    ),
+                    expressionVirtualColumn(
+                        "v1",
+                        "(\"__time\" + CAST(((\"m1\" - 3.5) * 1000), 'LONG'))",
+                        ColumnType.LONG
+                    )
+                )
+                .columns("v0", "v1")
+                .columnTypes(ColumnType.LONG, ColumnType.LONG)
+                
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+                .context(QUERY_CONTEXT_DEFAULT)
+                .build()
+        ),
+        ImmutableList.of(
+            new Object[]{timestamp("2000-01-01T12"), 
timestamp("1999-12-31T23:59:57.500")},
+            new Object[]{timestamp("2000-01-03"), 
timestamp("2000-01-01T23:59:58.500")},
+            new Object[]{timestamp("2000-01-04T12"), 
timestamp("2000-01-02T23:59:59.500")},
+            new Object[]{timestamp("2001-01-03"), 
timestamp("2001-01-01T00:00:00.500")},
+            new Object[]{timestamp("2001-01-04T12"), 
timestamp("2001-01-02T00:00:01.500")},
+            new Object[]{timestamp("2001-01-06"), 
timestamp("2001-01-03T00:00:02.500")}
+        )
+    );
+  }
+
   @Test
   public void testInfixCastMixedWithCastInGroupBy()
   {


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

Reply via email to