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);