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

abhishek pushed a commit to branch 29.0.0
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/29.0.0 by this push:
     new 115bee35aef Strict window frame checks (#15746) (#15827)
115bee35aef is described below

commit 115bee35aefe6babe9689c192424c0d0b641ec69
Author: Zoltan Haindrich <[email protected]>
AuthorDate: Mon Feb 5 10:44:11 2024 +0100

    Strict window frame checks (#15746) (#15827)
    
    introduce checks to ensure that window frame is supported
    added check to ensure that no expressions are set as bounds
    added logic to detect following/following like cases - described in Window 
function fails to demarcate if 2 following are used #15739
    currently RANGE frames are only supported correctly if both endpoints are 
unbounded or current row Offset based window range support #15767
    added windowingStrictValidation context key to provide a way to override 
the check
    
    (cherry picked from commit 8f5b7522c72f33afdd36c7b70e4de85f455d65a1)
---
 .../java/org/apache/druid/query/QueryContext.java  | 10 +++
 .../java/org/apache/druid/query/QueryContexts.java |  3 +
 .../org/apache/druid/query/QueryContextsTest.java  |  9 ++
 .../sql/calcite/planner/DruidSqlValidator.java     | 99 +++++++++++++++++++++-
 .../druid/sql/calcite/planner/PlannerContext.java  |  2 +-
 .../druid/sql/calcite/BaseCalciteQueryTest.java    | 10 +++
 .../apache/druid/sql/calcite/CalciteQueryTest.java | 46 ++++++++++
 .../druid/sql/calcite/CalciteWindowQueryTest.java  |  7 +-
 .../druid/sql/calcite/DrillWindowQueryTest.java    |  2 +-
 .../calcite/expression/ExpressionTestHelper.java   |  4 +-
 10 files changed, 185 insertions(+), 7 deletions(-)

diff --git a/processing/src/main/java/org/apache/druid/query/QueryContext.java 
b/processing/src/main/java/org/apache/druid/query/QueryContext.java
index 6fa73f0b65c..3364512052b 100644
--- a/processing/src/main/java/org/apache/druid/query/QueryContext.java
+++ b/processing/src/main/java/org/apache/druid/query/QueryContext.java
@@ -27,6 +27,7 @@ import org.apache.druid.query.QueryContexts.Vectorize;
 import org.apache.druid.segment.QueryableIndexStorageAdapter;
 
 import javax.annotation.Nullable;
+
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
@@ -582,6 +583,15 @@ public class QueryContext
     );
   }
 
+  public boolean isWindowingStrictValidation()
+  {
+    return getBoolean(
+        QueryContexts.WINDOWING_STRICT_VALIDATION,
+        QueryContexts.DEFAULT_WINDOWING_STRICT_VALIDATION
+    );
+  }
+
+
   public String getBrokerServiceName()
   {
     return getString(QueryContexts.BROKER_SERVICE_NAME);
diff --git a/processing/src/main/java/org/apache/druid/query/QueryContexts.java 
b/processing/src/main/java/org/apache/druid/query/QueryContexts.java
index 7d39edfde66..0359b13e2f0 100644
--- a/processing/src/main/java/org/apache/druid/query/QueryContexts.java
+++ b/processing/src/main/java/org/apache/druid/query/QueryContexts.java
@@ -85,6 +85,8 @@ public class QueryContexts
   public static final String SERIALIZE_DATE_TIME_AS_LONG_INNER_KEY = 
"serializeDateTimeAsLongInner";
   public static final String UNCOVERED_INTERVALS_LIMIT_KEY = 
"uncoveredIntervalsLimit";
   public static final String MIN_TOP_N_THRESHOLD = "minTopNThreshold";
+  public static final String WINDOWING_STRICT_VALIDATION = 
"windowingStrictValidation";
+
 
   // SQL query context keys
   public static final String CTX_SQL_QUERY_ID = BaseQuery.SQL_QUERY_ID;
@@ -117,6 +119,7 @@ public class QueryContexts
   public static final boolean DEFAULT_ENABLE_DEBUG = false;
   public static final int DEFAULT_IN_SUB_QUERY_THRESHOLD = Integer.MAX_VALUE;
   public static final boolean DEFAULT_ENABLE_TIME_BOUNDARY_PLANNING = false;
+  public static final boolean DEFAULT_WINDOWING_STRICT_VALIDATION = true;
 
   @SuppressWarnings("unused") // Used by Jackson serialization
   public enum Vectorize
diff --git 
a/processing/src/test/java/org/apache/druid/query/QueryContextsTest.java 
b/processing/src/test/java/org/apache/druid/query/QueryContextsTest.java
index 38b5384ded9..09496ece9f2 100644
--- a/processing/src/test/java/org/apache/druid/query/QueryContextsTest.java
+++ b/processing/src/test/java/org/apache/druid/query/QueryContextsTest.java
@@ -151,6 +151,15 @@ public class QueryContextsTest
     );
   }
 
+  @Test
+  public void testDefaultWindowingStrictValidation()
+  {
+    Assert.assertEquals(
+        QueryContexts.DEFAULT_WINDOWING_STRICT_VALIDATION,
+        QueryContext.empty().isWindowingStrictValidation()
+    );
+  }
+
   @Test
   public void testGetEnableJoinLeftScanDirect()
   {
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
index 34f3c7410f3..390ddf96baf 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
@@ -25,13 +25,19 @@ import org.apache.calcite.prepare.CalciteCatalogReader;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperatorTable;
+import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql.SqlWindow;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.apache.calcite.util.Util;
 import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.query.QueryContexts;
 import org.apache.druid.sql.calcite.run.EngineFeature;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Druid extended SQL validator. (At present, it doesn't actually
@@ -53,6 +59,97 @@ class DruidSqlValidator extends BaseDruidSqlValidator
     this.plannerContext = plannerContext;
   }
 
+  @Override
+  public void validateWindow(SqlNode windowOrId, SqlValidatorScope scope, 
@Nullable SqlCall call)
+  {
+    final SqlWindow targetWindow;
+    switch (windowOrId.getKind()) {
+      case IDENTIFIER:
+        targetWindow = getWindowByName((SqlIdentifier) windowOrId, scope);
+        break;
+      case WINDOW:
+        targetWindow = (SqlWindow) windowOrId;
+        break;
+      default:
+        throw Util.unexpected(windowOrId.getKind());
+    }
+
+
+    @Nullable
+    SqlNode lowerBound = targetWindow.getLowerBound();
+    @Nullable
+    SqlNode upperBound = targetWindow.getUpperBound();
+    if (!isValidEndpoint(lowerBound) || !isValidEndpoint(upperBound)) {
+      throw buildCalciteContextException(
+          "Window frames with expression based lower/upper bounds are not 
supported.",
+          windowOrId
+      );
+    }
+
+    if (isPrecedingOrFollowing(lowerBound) &&
+        isPrecedingOrFollowing(upperBound) &&
+        lowerBound.getKind() == upperBound.getKind()) {
+      // this limitation can be lifted when 
https://github.com/apache/druid/issues/15739 is addressed
+      throw buildCalciteContextException(
+          "Query bounds with both lower and upper bounds as PRECEDING or 
FOLLOWING is not supported.",
+          windowOrId
+      );
+    }
+
+    if (plannerContext.queryContext().isWindowingStrictValidation()) {
+      if (!targetWindow.isRows() &&
+          (!isValidRangeEndpoint(lowerBound) || 
!isValidRangeEndpoint(upperBound))) {
+        // this limitation can be lifted when 
https://github.com/apache/druid/issues/15767 is addressed
+        throw buildCalciteContextException(
+            StringUtils.format(
+                "The query contains a window frame which may return incorrect 
results. To disregard this warning, set [%s] to false in the query context.",
+                QueryContexts.WINDOWING_STRICT_VALIDATION
+            ),
+            windowOrId
+        );
+      }
+    }
+
+    super.validateWindow(windowOrId, scope, call);
+  }
+
+  private boolean isPrecedingOrFollowing(@Nullable SqlNode bound)
+  {
+    if (bound == null) {
+      return false;
+    }
+    SqlKind kind = bound.getKind();
+    return kind == SqlKind.PRECEDING || kind == SqlKind.FOLLOWING;
+  }
+
+  /**
+   * Checks if the given endpoint is acceptable.
+   */
+  private boolean isValidEndpoint(@Nullable SqlNode bound)
+  {
+    if (isValidRangeEndpoint(bound)) {
+      return true;
+    }
+    if (bound.getKind() == SqlKind.FOLLOWING || bound.getKind() == 
SqlKind.PRECEDING) {
+      final SqlNode boundVal = ((SqlCall) bound).operand(0);
+      if (SqlUtil.isLiteral(boundVal)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Checks if the given endpoint is valid for a RANGE window frame.
+   */
+  private boolean isValidRangeEndpoint(@Nullable SqlNode bound)
+  {
+    return bound == null
+        || SqlWindow.isCurrentRow(bound)
+        || SqlWindow.isUnboundedFollowing(bound)
+        || SqlWindow.isUnboundedPreceding(bound);
+  }
+
   @Override
   public void validateCall(SqlCall call, SqlValidatorScope scope)
   {
@@ -80,7 +177,7 @@ class DruidSqlValidator extends BaseDruidSqlValidator
     super.validateCall(call, scope);
   }
 
-  private CalciteContextException buildCalciteContextException(String message, 
SqlCall call)
+  private CalciteContextException buildCalciteContextException(String message, 
SqlNode call)
   {
     SqlParserPos pos = call.getParserPosition();
     return new CalciteContextException(message,
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
index b184d513fca..feefdbcca89 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
@@ -85,7 +85,7 @@ public class PlannerContext
   public static final String CTX_SQL_OUTER_LIMIT = "sqlOuterLimit";
 
   /**
-   * Undocumented context key, used to enable window functions.
+   * Key to enable window functions.
    */
   public static final String CTX_ENABLE_WINDOW_FNS = "enableWindowing";
 
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
index eb2528a6d1c..83a863e33f0 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
@@ -89,11 +89,13 @@ import org.apache.druid.server.security.ResourceAction;
 import org.apache.druid.sql.SqlStatementFactory;
 import org.apache.druid.sql.calcite.QueryTestRunner.QueryResults;
 import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.ExpressionTestHelper;
 import org.apache.druid.sql.calcite.planner.Calcites;
 import org.apache.druid.sql.calcite.planner.PlannerConfig;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.planner.PlannerFactory;
 import org.apache.druid.sql.calcite.rule.ExtensionCalciteRuleProvider;
+import org.apache.druid.sql.calcite.run.EngineFeature;
 import org.apache.druid.sql.calcite.run.SqlEngine;
 import org.apache.druid.sql.calcite.schema.DruidSchemaManager;
 import org.apache.druid.sql.calcite.util.CalciteTestBase;
@@ -137,6 +139,7 @@ import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
 
 /**
  * A base class for SQL query testing. It sets up query execution environment, 
provides useful helper methods,
@@ -739,6 +742,13 @@ public class BaseCalciteQueryTest extends CalciteTestBase
     basePlannerComponentSupplier.finalizePlanner(plannerFixture);
   }
 
+  public void assumeFeatureAvailable(EngineFeature feature)
+  {
+    boolean featureAvailable = queryFramework().engine()
+        .featureAvailable(feature, ExpressionTestHelper.PLANNER_CONTEXT);
+    assumeTrue(StringUtils.format("test disabled; feature [%s] is not 
available!", feature), featureAvailable);
+  }
+
   public void assertQueryIsUnplannable(final String sql, String expectedError)
   {
     assertQueryIsUnplannable(PLANNER_CONFIG_DEFAULT, sql, expectedError);
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index afe67eb6978..1c9c263817d 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -118,6 +118,7 @@ import org.apache.druid.sql.calcite.planner.Calcites;
 import org.apache.druid.sql.calcite.planner.PlannerConfig;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.rel.CannotBuildQueryException;
+import org.apache.druid.sql.calcite.run.EngineFeature;
 import org.apache.druid.sql.calcite.util.CalciteTests;
 import org.apache.druid.sql.calcite.util.TestDataBuilder;
 import org.hamcrest.CoreMatchers;
@@ -14864,6 +14865,51 @@ public class CalciteQueryTest extends 
BaseCalciteQueryTest
     assertThat(e, invalidSqlIs("ASCENDING ordering with NULLS LAST is not 
supported! (line [1], column [41])"));
   }
 
+  @Test
+  public void testUnSupportedRangeBounds()
+  {
+    assumeFeatureAvailable(EngineFeature.WINDOW_FUNCTIONS);
+
+    DruidException e = assertThrows(DruidException.class, () -> testBuilder()
+        .queryContext(ImmutableMap.of(PlannerContext.CTX_ENABLE_WINDOW_FNS, 
true))
+        .sql("SELECT dim1,ROW_NUMBER() OVER (ORDER BY dim1 RANGE BETWEEN 3 
PRECEDING AND 2 FOLLOWING) from druid.foo")
+        .run());
+    assertThat(e, invalidSqlIs("The query contains a window frame which may 
return incorrect results. To disregard this warning, set 
[windowingStrictValidation] to false in the query context. (line [1], column 
[31])"));
+  }
+
+  @Test
+  public void testUnSupportedWindowBoundExpressions()
+  {
+    assumeFeatureAvailable(EngineFeature.WINDOW_FUNCTIONS);
+
+    DruidException e = assertThrows(DruidException.class, () -> testBuilder()
+        .queryContext(ImmutableMap.of(PlannerContext.CTX_ENABLE_WINDOW_FNS, 
true))
+        .sql("SELECT dim1,ROW_NUMBER() OVER (ORDER BY dim1 ROWS BETWEEN dim1 
PRECEDING AND dim1 FOLLOWING) from druid.foo")
+        .run());
+    assertThat(e, invalidSqlIs("Window frames with expression based 
lower/upper bounds are not supported. (line [1], column [31])"));
+  }
+
+
+  @Test
+  public void testUnSupportedWindowBoundTypes()
+  {
+    assumeFeatureAvailable(EngineFeature.WINDOW_FUNCTIONS);
+
+    DruidException e;
+    e = assertThrows(DruidException.class, () -> testBuilder()
+        .queryContext(ImmutableMap.of(PlannerContext.CTX_ENABLE_WINDOW_FNS, 
true))
+        .sql("SELECT dim1,ROW_NUMBER() OVER (ORDER BY dim1 ROWS BETWEEN 1 
PRECEDING AND 1 PRECEDING) from druid.foo")
+        .run());
+    assertThat(e, invalidSqlIs("Query bounds with both lower and upper bounds 
as PRECEDING or FOLLOWING is not supported. (line [1], column [31])"));
+
+    e = assertThrows(DruidException.class, () -> testBuilder()
+        .queryContext(ImmutableMap.of(PlannerContext.CTX_ENABLE_WINDOW_FNS, 
true))
+        .sql("SELECT dim1,ROW_NUMBER() OVER (ORDER BY dim1 ROWS BETWEEN 1 
FOLLOWING AND 1 FOLLOWING) from druid.foo")
+        .run());
+    assertThat(e, invalidSqlIs("Query bounds with both lower and upper bounds 
as PRECEDING or FOLLOWING is not supported. (line [1], column [31])"));
+  }
+
+
   @Test
   public void testWindowingErrorWithoutFeatureFlag()
   {
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteWindowQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteWindowQueryTest.java
index 32ffda0cc39..d6212e0bf95 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteWindowQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteWindowQueryTest.java
@@ -212,8 +212,11 @@ public class CalciteWindowQueryTest extends 
BaseCalciteQueryTest
       testBuilder()
           .skipVectorize(true)
           .sql(testCase.getSql())
-          .queryContext(ImmutableMap.of(PlannerContext.CTX_ENABLE_WINDOW_FNS, 
true,
-              QueryContexts.ENABLE_DEBUG, true))
+          .queryContext(ImmutableMap.of(
+              PlannerContext.CTX_ENABLE_WINDOW_FNS, true,
+              QueryContexts.ENABLE_DEBUG, true,
+              QueryContexts.WINDOWING_STRICT_VALIDATION, false
+              ))
           .addCustomVerification(QueryVerification.ofResults(testCase))
           .run();
     }
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/DrillWindowQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/DrillWindowQueryTest.java
index d343cfe9800..03dc361e478 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/DrillWindowQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/DrillWindowQueryTest.java
@@ -5593,8 +5593,8 @@ public class DrillWindowQueryTest extends 
BaseCalciteQueryTest
   public void test_lag_func_lag_Fn_67()
   {
     windowQueryTest();
-  }
 
+  }
   @NotYetSupported(Modes.UNSUPPORTED_NULL_ORDERING)
   @DrillTest("lag_func/lag_Fn_68")
   @Test
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java
index 093ca0278c5..6155678c845 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java
@@ -77,7 +77,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
-class ExpressionTestHelper
+public class ExpressionTestHelper
 {
   private static final JoinableFactoryWrapper JOINABLE_FACTORY_WRAPPER = 
CalciteTests.createJoinableFactoryWrapper();
   private static final PlannerToolbox PLANNER_TOOLBOX = new PlannerToolbox(
@@ -99,7 +99,7 @@ class ExpressionTestHelper
       CalciteTests.TEST_AUTHORIZER_MAPPER,
       AuthConfig.newBuilder().build()
   );
-  private static final PlannerContext PLANNER_CONTEXT = PlannerContext.create(
+  public static final PlannerContext PLANNER_CONTEXT = PlannerContext.create(
       PLANNER_TOOLBOX,
       "SELECT 1", // The actual query isn't important for this test
       null, /* Don't need engine */


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to