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

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new ef00dee7f4 [CALCITE-4543] Interval literal loses a fractional second 
when it has scale greater than 3
ef00dee7f4 is described below

commit ef00dee7f460f75b90d0e6a46123d97342348f41
Author: Aleksandr Efimov <[email protected]>
AuthorDate: Wed Sep 2 21:10:40 2026 +0300

    [CALCITE-4543] Interval literal loses a fractional second when it has scale 
greater than 3
---
 .../apache/calcite/sql/SqlIntervalQualifier.java   | 11 ++++-
 .../java/org/apache/calcite/sql/SqlLiteral.java    |  7 ++-
 .../apache/calcite/sql/parser/SqlParserUtil.java   | 51 +++++++++++++++++++---
 .../java/org/apache/calcite/sql/SqlNodeTest.java   | 30 +++++++++++++
 .../calcite/sql/parser/SqlParserUtilTest.java      | 33 ++++++++++++++
 5 files changed, 124 insertions(+), 8 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java 
b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
index 5f81211b99..133803207b 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
@@ -95,6 +95,7 @@ public class SqlIntervalQualifier extends SqlNode {
 
   private static final BigDecimal ZERO = BigDecimal.ZERO;
   private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000);
+  private static final BigDecimal NANOS_PER_MILLI = 
BigDecimal.valueOf(1000_000);
   private static final BigDecimal INT_MAX_VALUE_PLUS_ONE =
       BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE);
 
@@ -595,7 +596,7 @@ private static int[] fillDayTimeIntervalValueArray(
       BigDecimal minute,
       BigDecimal second,
       BigDecimal secondFrac) {
-    int[] ret = new int[6];
+    int[] ret = new int[7];
 
     ret[0] = sign;
     ret[1] = day.intValue();
@@ -603,6 +604,14 @@ private static int[] fillDayTimeIntervalValueArray(
     ret[3] = minute.intValue();
     ret[4] = second.intValue();
     ret[5] = secondFrac.intValue();
+    // secondFrac is expressed in milliseconds and, for a qualifier whose
+    // fractional second precision is greater than 3, carries digits that
+    // element 5 cannot hold. Keep those digits as a number of nanoseconds
+    // below the millisecond, so that element 5 keeps its meaning and a
+    // caller that needs the exact value can reconstruct it.
+    ret[6] =
+        secondFrac.subtract(BigDecimal.valueOf(ret[5]))
+            .multiply(NANOS_PER_MILLI).intValue();
 
     return ret;
   }
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java 
b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
index d0af5ba652..09ba2b33d5 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
@@ -395,7 +395,12 @@ public <T extends Object> T getValueAs(Class<T> clazz) {
         return clazz.cast(valTime.getSign()
             * SqlParserUtil.intervalToMillis(valTime));
       } else if (clazz == BigDecimal.class) {
-        return clazz.cast(BigDecimal.valueOf(getValueAs(Long.class)));
+        // Not via Long: a qualifier may declare a fractional second precision
+        // greater than 3, and those digits do not survive a whole number of
+        // milliseconds.
+        return clazz.cast(
+            SqlParserUtil.intervalToExactMillis(valTime)
+                .multiply(BigDecimal.valueOf(valTime.getSign())));
       } else if (clazz == TimeUnitRange.class) {
         return clazz.cast(qualifier.timeUnitRange);
       } else if (clazz == TimeUnit.class) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java 
b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index 3c0b6bc99b..7bc3ee360c 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -527,17 +527,56 @@ public static long intervalToMillis(
   public static long intervalToMillis(
       String literal,
       SqlIntervalQualifier intervalQualifier) {
+    final int[] ret = evaluateDayTimeLiteral(literal, intervalQualifier);
+    return ret[0] * dayTimeToMillis(ret);
+  }
+
+  /**
+   * Converts the interval value into a millisecond representation that keeps
+   * the digits below the millisecond.
+   *
+   * <p>{@link #intervalToMillis(SqlIntervalLiteral.IntervalValue)} rounds
+   * towards zero, so a literal whose qualifier declares a fractional second
+   * precision greater than 3 loses the digits it was allowed to declare. This
+   * method reports them.
+   *
+   * @param interval Interval
+   * @return a value that represents the millisecond equivalent of the
+   * interval value, with a fraction if the literal has one
+   */
+  public static BigDecimal intervalToExactMillis(
+      SqlIntervalLiteral.IntervalValue interval) {
+    return intervalToExactMillis(
+        interval.getIntervalLiteral(),
+        interval.getIntervalQualifier());
+  }
+
+  public static BigDecimal intervalToExactMillis(
+      String literal,
+      SqlIntervalQualifier intervalQualifier) {
+    final int[] ret = evaluateDayTimeLiteral(literal, intervalQualifier);
+    final BigDecimal wholeMillis = BigDecimal.valueOf(dayTimeToMillis(ret));
+    // Keep the scale at 0 unless the literal really has digits below the
+    // millisecond, so that a plan for an ordinary interval is unchanged.
+    final BigDecimal millis =
+        ret[6] == 0 ? wholeMillis : wholeMillis.add(BigDecimal.valueOf(ret[6], 
6));
+    return ret[0] < 0 ? millis.negate() : millis;
+  }
+
+  private static int[] evaluateDayTimeLiteral(String literal,
+      SqlIntervalQualifier intervalQualifier) {
     checkArgument(!intervalQualifier.isYearMonth(),
         "interval must be day time");
-    int[] ret;
     try {
-      ret =
-          intervalQualifier.evaluateIntervalLiteral(literal,
-              intervalQualifier.getParserPosition(), 
RelDataTypeSystem.DEFAULT);
+      return intervalQualifier.evaluateIntervalLiteral(literal,
+          intervalQualifier.getParserPosition(), RelDataTypeSystem.DEFAULT);
     } catch (CalciteContextException e) {
       throw new RuntimeException("while parsing day-to-second interval "
           + literal, e);
     }
+  }
+
+  private static long dayTimeToMillis(int[] ret) {
     long l = 0;
     long[] conv = new long[5];
     conv[4] = 1; // millisecond
@@ -545,10 +584,10 @@ public static long intervalToMillis(
     conv[2] = conv[3] * 60; // minute
     conv[1] = conv[2] * 60; // hour
     conv[0] = conv[1] * 24; // day
-    for (int i = 1; i < ret.length; i++) {
+    for (int i = 1; i <= conv.length; i++) {
       l += conv[i - 1] * ret[i];
     }
-    return ret[0] * l;
+    return l;
   }
 
   /**
diff --git a/core/src/test/java/org/apache/calcite/sql/SqlNodeTest.java 
b/core/src/test/java/org/apache/calcite/sql/SqlNodeTest.java
index 75311d2612..6acd3ffded 100644
--- a/core/src/test/java/org/apache/calcite/sql/SqlNodeTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/SqlNodeTest.java
@@ -29,6 +29,7 @@
 import org.hamcrest.Matcher;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -96,6 +97,35 @@ class SqlNodeTest {
         containsString("exceeds the configured plain-notation bound"));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4543";>[CALCITE-4543]
+   * Interval literal loses a fractional second when it has scale greater
+   * than 3</a>. */
+  @Test void testIntervalLiteralKeepsSubMillisecondFraction()
+      throws SqlParseException {
+    final SqlLiteral second9 =
+        (SqlLiteral) parseExpression("INTERVAL '1.123456789' SECOND(1,9)");
+    assertThat(second9.getValueAs(BigDecimal.class),
+        is(new BigDecimal("1123.456789")));
+
+    final SqlLiteral negative =
+        (SqlLiteral) parseExpression("INTERVAL -'1.123456789' SECOND(1,9)");
+    assertThat(negative.getValueAs(BigDecimal.class),
+        is(new BigDecimal("-1123.456789")));
+
+    final SqlLiteral dayToSecond9 =
+        (SqlLiteral) parseExpression(
+            "INTERVAL '1 02:03:04.123456789' DAY(2) TO SECOND(9)");
+    assertThat(dayToSecond9.getValueAs(BigDecimal.class),
+        is(new BigDecimal("93784123.456789")));
+
+    // A qualifier that declares nothing below the millisecond is unaffected
+    final SqlLiteral second3 =
+        (SqlLiteral) parseExpression("INTERVAL '1.123' SECOND(1,3)");
+    assertThat(second3.getValueAs(BigDecimal.class),
+        is(new BigDecimal("1123")));
+  }
+
   private static Matcher<String> isEqualsDeep(String sqlExpected) {
     return new CustomTypeSafeMatcher<String>("isDeepEqual") {
       @Override protected boolean matchesSafely(String sqlActual) {
diff --git 
a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java 
b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
index d4e9f30d48..3b9fcecec6 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
@@ -24,6 +24,8 @@
 
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
+
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -43,6 +45,37 @@ public class SqlParserUtilTest {
     assertThat(SqlParserUtil.intervalToMillis("2.1", qualifier), 
equalTo(2_100L));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4543";>[CALCITE-4543]
+   * Interval literal loses a fractional second when it has scale greater
+   * than 3</a>. */
+  @Test void testSecondIntervalToExactMillis() {
+    final SqlIntervalQualifier qualifier =
+        new SqlIntervalQualifier(TimeUnit.SECOND, 1, TimeUnit.SECOND, 9,
+            POSITION);
+    assertThat(SqlParserUtil.intervalToMillis("1.123456789", qualifier),
+        equalTo(1_123L));
+    assertThat(SqlParserUtil.intervalToExactMillis("1.123456789", qualifier),
+        equalTo(new BigDecimal("1123.456789")));
+    assertThat(SqlParserUtil.intervalToExactMillis("-1.123456789", qualifier),
+        equalTo(new BigDecimal("-1123.456789")));
+  }
+
+  @Test void testDayToSecondIntervalToExactMillis() {
+    final SqlIntervalQualifier qualifier =
+        new SqlIntervalQualifier(TimeUnit.DAY, 2, TimeUnit.SECOND, 9, 
POSITION);
+    assertThat(
+        SqlParserUtil.intervalToExactMillis("1 02:03:04.123456789", qualifier),
+        equalTo(new BigDecimal("93784123.456789")));
+  }
+
+  @Test void testIntervalToExactMillisWithoutFraction() {
+    final SqlIntervalQualifier qualifier =
+        new SqlIntervalQualifier(TimeUnit.SECOND, null, POSITION);
+    assertThat(SqlParserUtil.intervalToExactMillis("2", qualifier),
+        equalTo(new BigDecimal("2000")));
+  }
+
   @Test void testMinuteIntervalToMillis() {
     final SqlIntervalQualifier qualifier =
         new SqlIntervalQualifier(TimeUnit.MINUTE, null, POSITION);

Reply via email to