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 b3527ee546 [CALCITE-7692] FLOOR/CEIL of INTERVAL produces wrong results
b3527ee546 is described below
commit b3527ee546f3a12d7c0c2ff75a10d3805e4cc747
Author: Mihai Budiu <[email protected]>
AuthorDate: Wed Aug 5 15:38:27 2026 -0700
[CALCITE-7692] FLOOR/CEIL of INTERVAL produces wrong results
Signed-off-by: Mihai Budiu <[email protected]>
---
.../calcite/sql2rel/StandardConvertletTable.java | 66 ++++++++++++----------
.../apache/calcite/test/SqlToRelConverterTest.java | 31 ++++++++++
.../apache/calcite/test/SqlToRelConverterTest.xml | 12 ++++
core/src/test/resources/sql/operator.iq | 41 ++++++++++++++
site/_docs/reference.md | 8 +--
.../org/apache/calcite/test/SqlOperatorTest.java | 19 ++++---
6 files changed, 135 insertions(+), 42 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index 92e0739624..bff49f3924 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -830,36 +830,44 @@ protected RexNode convertCast(
protected RexNode convertFloorCeil(SqlRexContext cx, SqlCall call) {
final boolean floor = call.getKind() == SqlKind.FLOOR;
final SqlParserPos pos = call.getParserPosition();
- // Rewrite floor, ceil of interval
- if (call.operandCount() == 1
- && call.operand(0) instanceof SqlIntervalLiteral) {
- final SqlIntervalLiteral literal = call.operand(0);
- SqlIntervalLiteral.IntervalValue interval =
- literal.getValueAs(SqlIntervalLiteral.IntervalValue.class);
- BigDecimal val =
- interval.getIntervalQualifier().getStartUnit().multiplier;
- RexNode rexInterval = cx.convertExpression(literal);
-
+ // Rewrite floor, ceil of an interval as arithmetic that rounds to a
+ // multiple of the interval's leading unit.
+ if (call.operandCount() == 1) {
final RexBuilder rexBuilder = cx.getRexBuilder();
- RexNode zero = rexBuilder.makeExactLiteral(BigDecimal.valueOf(0));
- RexNode cond = ge(pos, rexBuilder, rexInterval, zero);
-
- RexNode pad =
- rexBuilder.makeExactLiteral(val.subtract(BigDecimal.ONE));
- RexNode cast =
- rexBuilder.makeReinterpretCast(pos, rexInterval.getType(), pad,
- rexBuilder.makeLiteral(false));
- RexNode sum =
- floor ? minus(pos, rexBuilder, rexInterval, cast)
- : plus(pos, rexBuilder, rexInterval, cast);
-
- RexNode kase = floor
- ? case_(rexBuilder, rexInterval, cond, sum)
- : case_(rexBuilder, sum, cond, rexInterval);
-
- RexNode factor = rexBuilder.makeExactLiteral(val);
- RexNode div = divideInt(pos, rexBuilder, kase, factor);
- return multiply(pos, rexBuilder, div, factor);
+ final RexNode rexInterval = cx.convertExpression(call.operand(0));
+ final SqlIntervalQualifier qualifier =
+ rexInterval.getType().getIntervalQualifier();
+ if (qualifier != null) {
+ if (qualifier.timeFrameName != null) {
+ throw new UnsupportedOperationException((floor ? "FLOOR" : "CEIL")
+ + " of an interval with custom time frame '"
+ + qualifier.timeFrameName + "' is not supported");
+ }
+ if (!RexUtil.isDeterministic(rexInterval)) {
+ throw new UnsupportedOperationException((floor ? "FLOOR" : "CEIL")
+ + " of a non-deterministic interval expression is not"
+ + " supported");
+ }
+ BigDecimal val = qualifier.getStartUnit().multiplier;
+ RexNode zero = rexBuilder.makeExactLiteral(BigDecimal.valueOf(0));
+ RexNode cond = ge(pos, rexBuilder, rexInterval, zero);
+
+ RexNode pad =
+ rexBuilder.makeIntervalLiteral(val.subtract(BigDecimal.ONE),
+ qualifier);
+ RexNode sum =
+ floor ? minus(pos, rexBuilder, rexInterval, pad)
+ : plus(pos, rexBuilder, rexInterval, pad);
+
+ // CASE operands are (when, then, else)
+ RexNode kase = floor
+ ? case_(rexBuilder, cond, rexInterval, sum)
+ : case_(rexBuilder, cond, sum, rexInterval);
+
+ RexNode factor = rexBuilder.makeExactLiteral(val);
+ RexNode div = divideInt(pos, rexBuilder, kase, factor);
+ return multiply(pos, rexBuilder, div, factor);
+ }
}
// normal floor, ceil function
diff --git
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 928c29850a..2f4ba89194 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -81,6 +81,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Unit test for {@link org.apache.calcite.sql2rel.SqlToRelConverter}.
@@ -6304,6 +6305,36 @@ void checkUserDefinedOrderByOver(NullCollation
nullCollation) {
assertThat(plan, containsString("FLOOR($4, FLAG(WEEK))"));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7692">[CALCITE-7692]
+ * FLOOR/CEIL of INTERVAL produces wrong results</a>.
+ *
+ * <p>FLOOR and CEIL of an interval expression, literal or not, are rewritten
+ * as arithmetic that rounds to a multiple of the interval's leading unit. */
+ @Test void testFloorCeilOfInterval() {
+ final String sql = "select floor(x) as f, ceil(x) as c\n"
+ + "from (values (interval '3:4:5' hour to second)) as t(x)";
+ sql(sql).ok();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7692">[CALCITE-7692]
+ * FLOOR/CEIL of INTERVAL produces wrong results</a>.
+ *
+ * <p>The rewrite evaluates its operand more than once, which is unsound
+ * for a non-deterministic operand; conversion must fail rather than
+ * produce incorrect results. */
+ @Test void testFloorOfNonDeterministicInterval() {
+ final String sql = "select floor(x * rand()) as f\n"
+ + "from (values (interval '3:4:5' hour to second)) as t(x)";
+ final UnsupportedOperationException e =
+ assertThrows(UnsupportedOperationException.class,
+ () -> sql(sql).toRel());
+ assertThat(e.getMessage(),
+ is("FLOOR of a non-deterministic interval expression is not"
+ + " supported"));
+ }
+
/** Test case of
* <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
* Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
diff --git
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 2cb1a10933..a8da739aa9 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -2624,6 +2624,18 @@ LogicalSort(fetch=[+(1, ABS(-2))])
<![CDATA[
LogicalProject(EXPR$0=[ROW(ITEM($3, 1).EMPNO, ITEM($3, 1).ENAME, ROW(ITEM($3,
1).DETAIL.SKILLS))])
LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testFloorCeilOfInterval">
+ <Resource name="sql">
+ <![CDATA[select floor(x) as f, ceil(x) as c
+from (values (interval '3:4:5' hour to second)) as t(x)]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(F=[*(/INT(CASE(>=($0, 0), $0, -($0, 3599999)), 3600000),
3600000)], C=[*(/INT(CASE(>=($0, 0), +($0, 3599999), $0), 3600000), 3600000)])
+ LogicalValues(tuples=[[{ 11045000 }]])
]]>
</Resource>
</TestCase>
diff --git a/core/src/test/resources/sql/operator.iq
b/core/src/test/resources/sql/operator.iq
index 41a470e5f9..45bee30f0c 100644
--- a/core/src/test/resources/sql/operator.iq
+++ b/core/src/test/resources/sql/operator.iq
@@ -842,4 +842,45 @@ SELECT
!ok
+# [CALCITE-7692] FLOOR/CEIL of INTERVAL produces wrong results
+# FLOOR and CEIL of an interval round to the interval's leading unit,
+# whether or not the operand is a literal.
+select floor(x) = interval '3' hour as f,
+ ceil(x) = interval '4' hour as c
+from (values (interval '3:4:5' hour to second)) as t(x);
++------+------+
+| F | C |
++------+------+
+| true | true |
++------+------+
+(1 row)
+
+!ok
+
+select floor(interval '-6.3' second) = interval '-7' second as fneg,
+ ceil(interval '-6.3' second) = interval '-6' second as cneg,
+ floor(interval '5-1' year to month) = interval '5' year as fym,
+ ceil(interval '-5-1' year to month) = interval '-5' year as cym;
++------+------+------+------+
+| FNEG | CNEG | FYM | CYM |
++------+------+------+------+
+| true | true | true | true |
++------+------+------+------+
+(1 row)
+
+!ok
+
+# The operand's interval type may be computed rather than declared; here
+# HOUR + MINUTE yields INTERVAL HOUR TO MINUTE, whose leading unit is HOUR.
+select floor(interval '2' hour + interval '90' minute) = interval '3' hour as
fa,
+ ceil(interval '2' hour + interval '90' minute) = interval '4' hour as ca;
++------+------+
+| FA | CA |
++------+------+
+| true | true |
++------+------+
+(1 row)
+
+!ok
+
# End operator.iq
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 4b198449ae..fda50d5bfc 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1608,6 +1608,8 @@ ### Date/time functions
| EXTRACT(timeUnit FROM datetime) | Extracts and returns the value of a
specified datetime field from a datetime value expression
| FLOOR(datetime TO timeUnit) | Rounds *datetime* down to *timeUnit*
| CEIL(datetime TO timeUnit) | Rounds *datetime* up to *timeUnit*
+| FLOOR(interval) | Rounds *interval* down to a multiple of its leading time
unit; for example, `FLOOR(INTERVAL '3:04:05' HOUR TO SECOND)` returns `INTERVAL
'3:00:00' HOUR TO SECOND`
+| CEIL(interval) | Rounds *interval* up to a multiple of its leading time unit
| YEAR(date) | Equivalent to `EXTRACT(YEAR FROM date)`. Returns
an integer.
| QUARTER(date) | Equivalent to `EXTRACT(QUARTER FROM date)`.
Returns an integer between 1 and 4.
| MONTH(date) | Equivalent to `EXTRACT(MONTH FROM date)`.
Returns an integer between 1 and 12.
@@ -1628,12 +1630,6 @@ ### Date/time functions
Not implemented:
-* CEIL(interval)
-* FLOOR(interval)
-* \+ interval
-* \- interval
-* interval + interval
-* interval - interval
* interval / interval
### System functions
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index f0ad6867fa..89a5ad1dad 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -14085,11 +14085,11 @@ private static void
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
f.checkNull("ceiling(cast(null as double))");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7692">[CALCITE-7692]
+ * FLOOR/CEIL of INTERVAL produces wrong results</a>. */
@Test void testCeilFuncInterval() {
final SqlOperatorFixture f = fixture();
- if (!f.brokenTestsEnabled()) {
- return;
- }
f.checkScalar("ceil(interval '3:4:5' hour to second)",
"+4:00:00.000000", "INTERVAL HOUR TO SECOND NOT NULL");
f.checkScalar("ceil(interval '-6.3' second)",
@@ -14318,11 +14318,11 @@ private static void
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
"-4", "INTEGER NOT NULL");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7692">[CALCITE-7692]
+ * FLOOR/CEIL of INTERVAL produces wrong results</a>. */
@Test void testFloorFuncInterval() {
final SqlOperatorFixture f = fixture();
- if (!f.brokenTestsEnabled()) {
- return;
- }
f.checkScalar("floor(interval '3:4:5' hour to second)",
"+3:00:00.000000",
"INTERVAL HOUR TO SECOND NOT NULL");
@@ -14332,6 +14332,12 @@ private static void
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
"+5-00", "INTERVAL YEAR TO MONTH NOT NULL");
f.checkScalar("floor(interval '-5-1' year to month)",
"-6-00", "INTERVAL YEAR TO MONTH NOT NULL");
+ f.checkNull("floor(cast(null as interval year))");
+ if (!f.brokenTestsEnabled()) {
+ return;
+ }
+ // FLOOR(interval TO time unit) is not implemented; the validator accepts
+ // only DATE, TIME and TIMESTAMP before TO.
f.checkScalar("floor(interval '-6.3' second to second)",
"-7.000000", "INTERVAL SECOND NOT NULL");
f.checkScalar("floor(interval '6-3' minute to second to minute)",
@@ -14348,7 +14354,6 @@ private static void
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
"201", "INTERVAL YEAR TO MONTH NOT NULL");
f.checkScalar("floor(interval '1004-1' year to month to millennium)",
"2001-00", "INTERVAL YEAR TO MONTH NOT NULL");
- f.checkNull("floor(cast(null as interval year))");
}
@Test void testTimestampAdd() {