This is an automated email from the ASF dual-hosted git repository.
mbudiu 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 e810d8becb [CALCITE-7418] SqlOverlapsOperator does not reject some
illegal comparisons (e.g., TIME vs DATE)
e810d8becb is described below
commit e810d8becb3544d141e7d4bf4fe65de24d0595c7
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Feb 16 21:21:20 2026 -0800
[CALCITE-7418] SqlOverlapsOperator does not reject some illegal comparisons
(e.g., TIME vs DATE)
Signed-off-by: Mihai Budiu <[email protected]>
---
.../calcite/sql/fun/SqlOverlapsOperator.java | 58 +++++++++++++++++++-
.../org/apache/calcite/test/SqlOperatorTest.java | 63 ++++++++++++++++++++++
2 files changed, 119 insertions(+), 2 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/sql/fun/SqlOverlapsOperator.java
b/core/src/main/java/org/apache/calcite/sql/fun/SqlOverlapsOperator.java
index 355f5c2ad5..550b6c65c5 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlOverlapsOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlOverlapsOperator.java
@@ -33,6 +33,8 @@
import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
/**
* SqlOverlapsOperator represents the SQL:1999 standard {@code OVERLAPS}
* function. Determines whether two anchored time intervals overlap.
@@ -77,8 +79,30 @@ void arg(SqlWriter writer, SqlCall call, int leftPrec, int
rightPrec, int i) {
return SqlOperandCountRanges.of(2);
}
+ /**
+ * Returns a template describing how the operator signature is to be built.
+ *
+ * @param operandsCount is used with functions that can take a variable
+ * number of operands
+ * @return signature template, where {0} is the operator name and {1}, {2},
etc are operands
+ */
+ @Override public @Nullable String getSignatureTemplate(final int
operandsCount) {
+ // This function can be called in 3 ways:
+ // - as a binary operator; format like a binary operator left OP right
+ // - as a ternary operator, for (a, b) CONTAINS c
+ // - as a quaternary operator, for (a, b) OVERLAPS (c, d)
+ if (operandsCount == 2) {
+ return "{1} {0} {2}";
+ } else if (operandsCount == 3) {
+ return "({1}, {2}) {0} {3}";
+ } else if (operandsCount == 4) {
+ return "({1}, {2}) {0} ({3}, {4})";
+ }
+ throw new IllegalArgumentException("Unexpected operand count " +
operandsCount);
+ }
+
@Override public String getAllowedSignatures(String opName) {
- final String d = "DATETIME";
+ final String d = "DT";
final String i = "INTERVAL";
String[] typeNames = {
d, d,
@@ -96,6 +120,16 @@ void arg(SqlWriter writer, SqlCall call, int leftPrec, int
rightPrec, int i) {
SqlUtil.getAliasedSignature(this, opName,
ImmutableList.of(d, typeNames[y], d, typeNames[y + 1])));
}
+ if (opName.equalsIgnoreCase("contains")) {
+ // Two more forms supported: (DT, DT) CONTAINS DT and (DT, INTERVAL)
CONTAINS DT
+ ret.append(NL);
+ ret.append(SqlUtil.getAliasedSignature(this, opName, ImmutableList.of(d,
d, d)));
+ ret.append(NL);
+ ret.append(SqlUtil.getAliasedSignature(this, opName, ImmutableList.of(d,
i, d)));
+ }
+ ret.append(NL);
+ ret.append("Where 'DT' is one of 'DATE', 'TIME', or 'TIMESTAMP', "
+ + "the same for all arguments.");
return ret.toString();
}
@@ -108,11 +142,21 @@ void arg(SqlWriter writer, SqlCall call, int leftPrec,
int rightPrec, int i) {
final SqlSingleOperandTypeChecker rightChecker;
switch (kind) {
case CONTAINS:
+ // A ternary call of the form (a, b) CONTAINS c
+ // OR a quaternary call of the form (a, b) CONTAINS (c, d)
rightChecker = OperandTypes.PERIOD_OR_DATETIME;
break;
- default:
+ case OVERLAPS:
+ case PRECEDES:
+ case IMMEDIATELY_PRECEDES:
+ case SUCCEEDS:
+ case IMMEDIATELY_SUCCEEDS:
+ case PERIOD_EQUALS:
+ // Always a quaternary call of the form (a, b) OVERLAPS (c, d)
rightChecker = OperandTypes.PERIOD;
break;
+ default:
+ throw new IllegalArgumentException("Unexpected operation " + kind);
}
if (!rightChecker.checkSingleOperandType(callBinding,
callBinding.operand(1), 0, throwOnFailure)) {
@@ -121,6 +165,7 @@ void arg(SqlWriter writer, SqlCall call, int leftPrec, int
rightPrec, int i) {
final RelDataType t0 = callBinding.getOperandType(0);
final RelDataType t1 = callBinding.getOperandType(1);
if (!SqlTypeUtil.isDatetime(t1)) {
+ // "quaternary" call, of the form (a, b) OVERLAPS (c, d)
final RelDataType t00 = t0.getFieldList().get(0).getType();
final RelDataType t10 = t1.getFieldList().get(0).getType();
if (!SqlTypeUtil.sameNamedType(t00, t10)) {
@@ -129,6 +174,15 @@ void arg(SqlWriter writer, SqlCall call, int leftPrec, int
rightPrec, int i) {
}
return false;
}
+ } else {
+ // "ternary" call, of the form (a, b) CONTAINS c
+ final RelDataType t00 = t0.getFieldList().get(0).getType();
+ if (!SqlTypeUtil.sameNamedType(t00, t1)) {
+ if (throwOnFailure) {
+ throw callBinding.newValidationSignatureError();
+ }
+ return false;
+ }
}
return true;
}
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 b3de8a3c51..41b45d3c76 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -3307,6 +3307,69 @@ static void checkOverlaps(OverlapChecker c) {
c.isTrue("($3,$0) IMMEDIATELY SUCCEEDS ($0,$0)");
}
+ /** Test cases for <a
href="https://issues.apache.org/jira/browse/CALCITE-7418">[CALCITE-7418]
+ * SqlOverlapsOperator does not reject some illegal comparisons (e.g., TIME
vs DATE)</a>. */
+ @Test void testNegativePeriodOperators() {
+ final String containsError = "Supported form\\(s\\): "
+ + "'\\(<DT>, <DT>\\) CONTAINS \\(<DT>, <DT>\\)'\\n"
+ + "'\\(<DT>, <DT>\\) CONTAINS \\(<DT>, <INTERVAL>\\)'\\n"
+ + "'\\(<DT>, <INTERVAL>\\) CONTAINS \\(<DT>, <DT>\\)'\\n"
+ + "'\\(<DT>, <INTERVAL>\\) CONTAINS \\(<DT>, <INTERVAL>\\)'\\n"
+ + "'\\(<DT>, <DT>\\) CONTAINS <DT>'\\n"
+ + "'\\(<DT>, <INTERVAL>\\) CONTAINS <DT>'\\n"
+ + "Where 'DT' is one of 'DATE', 'TIME', or 'TIMESTAMP', the same for
all arguments.";
+ final SqlOperatorFixture f = fixture();
+ f.checkFails("^(DATE '2020-10-10', DATE '2021-10-10') CONTAINS TIME
'10:00:00'^",
+ "Cannot apply 'CONTAINS' to arguments of type "
+ + "'<RECORDTYPE\\(DATE EXPR\\$0, DATE EXPR\\$1\\)> CONTAINS
<TIME\\(0\\)>'\\. "
+ + containsError, false);
+ f.checkFails("^(DATE '2020-10-10', DATE '2021-10-10') CONTAINS "
+ + "TIMESTAMP '2010-01-01 10:00:00'^",
+ "Cannot apply 'CONTAINS' to arguments of type "
+ + "'<RECORDTYPE\\(DATE EXPR\\$0, DATE EXPR\\$1\\)> CONTAINS
<TIMESTAMP\\(0\\)>'\\. "
+ + containsError, false);
+ f.checkFails("^(DATE '2020-10-10', TIMESTAMP '2021-10-10 00:00:00') "
+ + "CONTAINS TIMESTAMP '2010-01-01 10:00:00'^",
+ "Cannot apply 'CONTAINS' to arguments of type "
+ + "'<RECORDTYPE\\(DATE EXPR\\$0, TIMESTAMP\\(0\\) EXPR\\$1\\)> "
+ + "CONTAINS <TIMESTAMP\\(0\\)>'\\. "
+ + containsError, false);
+ f.checkFails("^(TIME '10:10:10', DATE '2021-10-10') CONTAINS TIME
'10:00:00'^",
+ "Cannot apply 'CONTAINS' to arguments of type "
+ + "'<RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE EXPR\\$1\\)> CONTAINS
<TIME\\(0\\)>'\\. "
+ + containsError, false);
+ f.checkFails("^(TIME '10:10:10', DATE '2021-10-10') CONTAINS TIMESTAMP
'2010-02-02 10:00:00'^",
+ "Cannot apply 'CONTAINS' to arguments of type "
+ + "'<RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE EXPR\\$1\\)> "
+ + "CONTAINS <TIMESTAMP\\(0\\)>'\\. "
+ + containsError, false);
+ final String overlapsError = "Supported form\\(s\\): "
+ + "'\\(<DT>, <DT>\\) OVERLAPS \\(<DT>, <DT>\\)'\\n"
+ + "'\\(<DT>, <DT>\\) OVERLAPS \\(<DT>, <INTERVAL>\\)'\\n"
+ + "'\\(<DT>, <INTERVAL>\\) OVERLAPS \\(<DT>, <DT>\\)'\\n"
+ + "'\\(<DT>, <INTERVAL>\\) OVERLAPS \\(<DT>, <INTERVAL>\\)'\\n"
+ + "Where 'DT' is one of 'DATE', 'TIME', or 'TIMESTAMP', the same for
all arguments.";
+ f.checkFails("^(TIME '10:10:10', DATE '2021-10-10') OVERLAPS "
+ + "(TIMESTAMP '2010-02-02 10:00:00', TIME '10:00:00')^",
+ "Cannot apply 'OVERLAPS' to arguments of type "
+ + "'<RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE EXPR\\$1\\)> "
+ + "OVERLAPS <RECORDTYPE\\(TIMESTAMP\\(0\\) EXPR\\$0, TIME\\(0\\)
EXPR\\$1\\)>'\\. "
+ + overlapsError, false);
+ f.checkFails("^(TIME '10:10:10', DATE '2021-10-10') "
+ + "OVERLAPS (TIME '10:00:00', DATE '2020-01-01')^",
+ "Cannot apply 'OVERLAPS' to arguments of type "
+ + "'<RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE EXPR\\$1\\)> "
+ + "OVERLAPS <RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE
EXPR\\$1\\)>'\\. "
+ + overlapsError, false);
+ final String precedesError = overlapsError.replace("OVERLAPS", "PRECEDES");
+ f.checkFails("^(TIME '10:10:10', DATE '2021-10-10') "
+ + "PRECEDES (TIME '10:00:00', TIME '10:10:10')^",
+ "Cannot apply 'PRECEDES' to arguments of type "
+ + "'<RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, DATE EXPR\\$1\\)> "
+ + "PRECEDES <RECORDTYPE\\(TIME\\(0\\) EXPR\\$0, TIME\\(0\\)
EXPR\\$1\\)>'\\. "
+ + precedesError, false);
+ }
+
@Test void testLessThanOperator() {
final SqlOperatorFixture f = fixture();
f.setFor(SqlStdOperatorTable.LESS_THAN, VmName.EXPAND);