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

Reply via email to