Repository: calcite
Updated Branches:
  refs/heads/master bdb953fa0 -> 0cbd2a182


[CALCITE-1900] Detect cyclic views and give useful error message


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/0cbd2a18
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/0cbd2a18
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/0cbd2a18

Branch: refs/heads/master
Commit: 0cbd2a1825768835dccce1b24f746bc968908c3a
Parents: bdb953f
Author: Julian Hyde <[email protected]>
Authored: Mon Jul 24 08:41:43 2017 -0700
Committer: Julian Hyde <[email protected]>
Committed: Mon Jul 24 20:19:12 2017 -0700

----------------------------------------------------------------------
 .../calcite/jdbc/CalciteConnectionImpl.java     |  4 ++
 .../org/apache/calcite/jdbc/CalcitePrepare.java | 21 +++++-
 .../apache/calcite/runtime/CalciteResource.java |  3 +
 .../java/org/apache/calcite/schema/Schemas.java | 46 ++++++++----
 .../calcite/schema/impl/ViewTableMacro.java     |  7 +-
 .../sql/validate/CyclicDefinitionException.java | 37 ++++++++++
 .../sql/validate/IdentifierNamespace.java       | 14 +++-
 .../calcite/runtime/CalciteResource.properties  |  1 +
 .../java/org/apache/calcite/test/JdbcTest.java  | 73 ++++++++++++++++++++
 9 files changed, 187 insertions(+), 19 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java 
b/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
index 97744a8..2df9253 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
@@ -471,6 +471,10 @@ abstract class CalciteConnectionImpl
           : ImmutableList.of(schemaName);
     }
 
+    public List<String> getObjectPath() {
+      return null;
+    }
+
     public CalciteConnectionConfig config() {
       return connection.config();
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java 
b/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
index 6ebd146..dee608d 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
@@ -41,6 +41,7 @@ import org.apache.calcite.runtime.Bindable;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.validate.CyclicDefinitionException;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.util.ImmutableIntList;
 
@@ -118,6 +119,13 @@ public interface CalcitePrepare {
     SparkHandler spark();
 
     DataContext getDataContext();
+
+    /** Returns the path of the object being analyzed, or null.
+     *
+     * <p>The object is being analyzed is typically a view. If it is already
+     * being analyzed further up the stack, the view definition can be deduced
+     * to be cylic. */
+    List<String> getObjectPath();
   }
 
   /** Callback to register Spark as the main engine. */
@@ -175,7 +183,17 @@ public interface CalcitePrepare {
     }
 
     public static void push(Context context) {
-      THREAD_CONTEXT_STACK.get().push(context);
+      final Deque<Context> stack = THREAD_CONTEXT_STACK.get();
+      final List<String> path = context.getObjectPath();
+      if (path != null) {
+        for (Context context1 : stack) {
+          final List<String> path1 = context1.getObjectPath();
+          if (path.equals(path1)) {
+            throw new CyclicDefinitionException(stack.size(), path);
+          }
+        }
+      }
+      stack.push(context);
     }
 
     public static Context peek() {
@@ -210,6 +228,7 @@ public interface CalcitePrepare {
         throw new UnsupportedOperationException();
       }
     }
+
   }
 
   /** The result of parsing and validating a SQL query. */

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java 
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index 84f21e1..b8afa01 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -636,6 +636,9 @@ public interface CalciteResource {
   @BaseMessage("Cannot stream VALUES")
   ExInst<SqlValidatorException> cannotStreamValues();
 
+  @BaseMessage("Cannot resolve ''{0}''; it references view ''{1}'', whose 
definition is cyclic")
+  ExInst<SqlValidatorException> cyclicDefinition(String id, String view);
+
   @BaseMessage("Modifiable view must be based on a single table")
   ExInst<SqlValidatorException> modifiableViewMustBeBasedOnSingleTable();
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/schema/Schemas.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/schema/Schemas.java 
b/core/src/main/java/org/apache/calcite/schema/Schemas.java
index 69cc44e..bd671f9 100644
--- a/core/src/main/java/org/apache/calcite/schema/Schemas.java
+++ b/core/src/main/java/org/apache/calcite/schema/Schemas.java
@@ -289,7 +289,7 @@ public final class Schemas {
     final ImmutableMap<CalciteConnectionProperty, String> propValues =
         ImmutableMap.of();
     final CalcitePrepare.Context context =
-        makeContext(connection, schema, schemaPath, propValues);
+        makeContext(connection, schema, schemaPath, null, propValues);
     CalcitePrepare.Dummy.push(context);
     try {
       return prepare.parse(context, sql);
@@ -307,7 +307,7 @@ public final class Schemas {
     final ImmutableMap<CalciteConnectionProperty, String> propValues =
         ImmutableMap.of();
     final CalcitePrepare.Context context =
-        makeContext(connection, schema, schemaPath, propValues);
+        makeContext(connection, schema, schemaPath, null, propValues);
     CalcitePrepare.Dummy.push(context);
     try {
       return prepare.convert(context, sql);
@@ -319,15 +319,16 @@ public final class Schemas {
   /** Analyzes a view. For use within Calcite only. */
   public static CalcitePrepare.AnalyzeViewResult analyzeView(
       final CalciteConnection connection, final CalciteSchema schema,
-      final List<String> schemaPath, final String sql, boolean fail) {
+      final List<String> schemaPath, final String viewSql,
+      List<String> viewPath, boolean fail) {
     final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
     final ImmutableMap<CalciteConnectionProperty, String> propValues =
         ImmutableMap.of();
     final CalcitePrepare.Context context =
-        makeContext(connection, schema, schemaPath, propValues);
+        makeContext(connection, schema, schemaPath, viewPath, propValues);
     CalcitePrepare.Dummy.push(context);
     try {
-      return prepare.analyzeView(context, sql, fail);
+      return prepare.analyzeView(context, viewSql, fail);
     } finally {
       CalcitePrepare.Dummy.pop(context);
     }
@@ -340,7 +341,7 @@ public final class Schemas {
       final ImmutableMap<CalciteConnectionProperty, String> map) {
     final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
     final CalcitePrepare.Context context =
-        makeContext(connection, schema, schemaPath, map);
+        makeContext(connection, schema, schemaPath, null, map);
     CalcitePrepare.Dummy.push(context);
     try {
       return prepare.prepareSql(context, CalcitePrepare.Query.of(sql),
@@ -350,21 +351,33 @@ public final class Schemas {
     }
   }
 
-  public static CalcitePrepare.Context makeContext(
-      final CalciteConnection connection, final CalciteSchema schema,
-      final List<String> schemaPath,
+  /**
+   * Creates a context for the purposes of preparing a statement.
+   *
+   * @param connection Connection
+   * @param schema Schema
+   * @param schemaPath Path wherein to look for functions
+   * @param objectPath Path of the object being analyzed (usually a view),
+   *                  or null
+   * @param propValues Connection properties
+   * @return Context
+   */
+  private static CalcitePrepare.Context makeContext(
+      CalciteConnection connection, CalciteSchema schema,
+      List<String> schemaPath, List<String> objectPath,
       final ImmutableMap<CalciteConnectionProperty, String> propValues) {
     if (connection == null) {
       final CalcitePrepare.Context context0 = CalcitePrepare.Dummy.peek();
       final CalciteConnectionConfig config =
           mutate(context0.config(), propValues);
       return makeContext(config, context0.getTypeFactory(),
-          context0.getDataContext(), schema, schemaPath);
+          context0.getDataContext(), schema, schemaPath, objectPath);
     } else {
       final CalciteConnectionConfig config =
           mutate(connection.config(), propValues);
       return makeContext(config, connection.getTypeFactory(),
-          createDataContext(connection, schema.root().plus()), schema, 
schemaPath);
+          createDataContext(connection, schema.root().plus()), schema,
+          schemaPath, objectPath);
     }
   }
 
@@ -383,7 +396,9 @@ public final class Schemas {
       final JavaTypeFactory typeFactory,
       final DataContext dataContext,
       final CalciteSchema schema,
-      final List<String> schemaPath) {
+      final List<String> schemaPath, final List<String> objectPath_) {
+    final ImmutableList<String> objectPath =
+        objectPath_ == null ? null : ImmutableList.copyOf(objectPath_);
     return new CalcitePrepare.Context() {
       public JavaTypeFactory getTypeFactory() {
         return typeFactory;
@@ -402,6 +417,10 @@ public final class Schemas {
         return schemaPath;
       }
 
+      public List<String> getObjectPath() {
+        return objectPath;
+      }
+
       public CalciteConnectionConfig config() {
         return connectionConfig;
       }
@@ -542,8 +561,7 @@ public final class Schemas {
     private final SchemaPlus rootSchema;
     private final ImmutableMap<String, Object> map;
 
-    public DummyDataContext(CalciteConnection connection,
-        SchemaPlus rootSchema) {
+    DummyDataContext(CalciteConnection connection, SchemaPlus rootSchema) {
       this.connection = connection;
       this.rootSchema = rootSchema;
       this.map =

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/schema/impl/ViewTableMacro.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/schema/impl/ViewTableMacro.java 
b/core/src/main/java/org/apache/calcite/schema/impl/ViewTableMacro.java
index 1b54e7a..5ded4c3 100644
--- a/core/src/main/java/org/apache/calcite/schema/impl/ViewTableMacro.java
+++ b/core/src/main/java/org/apache/calcite/schema/impl/ViewTableMacro.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.schema.impl;
 
 import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.jdbc.CalciteConnection;
 import org.apache.calcite.jdbc.CalcitePrepare;
 import org.apache.calcite.jdbc.CalciteSchema;
 import org.apache.calcite.rel.type.RelDataTypeImpl;
@@ -67,9 +68,11 @@ public class ViewTableMacro implements TableMacro {
   }
 
   public TranslatableTable apply(List<Object> arguments) {
+    final CalciteConnection connection =
+        MaterializedViewTable.MATERIALIZATION_CONNECTION;
     CalcitePrepare.AnalyzeViewResult parsed =
-        Schemas.analyzeView(MaterializedViewTable.MATERIALIZATION_CONNECTION,
-            schema, schemaPath, viewSql, modifiable != null && modifiable);
+        Schemas.analyzeView(connection, schema, schemaPath, viewSql, viewPath,
+            modifiable != null && modifiable);
     final List<String> schemaPath1 =
         schemaPath != null ? schemaPath : schema.path(null);
     if ((modifiable == null || modifiable)

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/sql/validate/CyclicDefinitionException.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/CyclicDefinitionException.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/CyclicDefinitionException.java
new file mode 100644
index 0000000..73096ef
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/CyclicDefinitionException.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql.validate;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/** Thrown when an object, such as a view, is found to have a cylic
+ * definition. */
+public class CyclicDefinitionException extends RuntimeException {
+  public final int depth;
+  public final List<String> path;
+
+  /** Creates CyclicDefinitionException. */
+  public CyclicDefinitionException(int depth, List<String> path) {
+    super("Cyclic object definition: " + path);
+    this.depth = depth;
+    this.path = ImmutableList.copyOf(path);
+  }
+}
+
+// End CyclicDefinitionException.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java 
b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
index 4f53fc0..e9e12e6 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
@@ -101,8 +101,18 @@ public class IdentifierNamespace extends AbstractNamespace 
{
     final SqlValidatorScope.ResolvedImpl resolved =
         new SqlValidatorScope.ResolvedImpl();
     final List<String> names = SqlIdentifier.toStar(id.names);
-    parentScope.resolveTable(names, nameMatcher,
-        SqlValidatorScope.Path.EMPTY, resolved);
+    try {
+      parentScope.resolveTable(names, nameMatcher,
+          SqlValidatorScope.Path.EMPTY, resolved);
+    } catch (CyclicDefinitionException e) {
+      if (e.depth == 1) {
+        throw validator.newValidationError(id,
+            RESOURCE.cyclicDefinition(id.toString(),
+                SqlIdentifier.getString(e.path)));
+      } else {
+        throw new CyclicDefinitionException(e.depth - 1, e.path);
+      }
+    }
     SqlValidatorScope.Resolve previousResolve = null;
     if (resolved.count() == 1) {
       final SqlValidatorScope.Resolve resolve =

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
----------------------------------------------------------------------
diff --git 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 360fb6e..9461fa4 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -206,6 +206,7 @@ StreamMustGroupByMonotonic=Streaming aggregation requires 
at least one monotonic
 StreamMustOrderByMonotonic=Streaming ORDER BY must start with monotonic 
expression
 StreamSetOpInconsistentInputs=Set operator cannot combine streaming and 
non-streaming inputs
 CannotStreamValues=Cannot stream VALUES
+CyclicDefinition=Cannot resolve ''{0}''; it references view ''{1}'', whose 
definition is cyclic
 ModifiableViewMustBeBasedOnSingleTable=Modifiable view must be based on a 
single table
 ModifiableViewMustHaveOnlyEqualityPredicates=Modifiable view must be 
predicated only on equality expressions
 MoreThanOneMappedColumn=View is not modifiable. More than one expression maps 
to column ''{0}'' of base table ''{1}''

http://git-wip-us.apache.org/repos/asf/calcite/blob/0cbd2a18/core/src/test/java/org/apache/calcite/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 825babe..8496e42 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -5534,6 +5534,79 @@ public class JdbcTest {
             + "name=Theodore\n");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-1900";>[CALCITE-1900]
+   * Improve error message for cyclic views</a>.
+   * Previously got a {@link StackOverflowError}. */
+  @Test public void testSelfReferentialView() throws Exception {
+    final CalciteAssert.AssertThat with =
+        modelWithView("select * from \"V\"", null);
+    with.query("select \"name\" from \"adhoc\".V")
+        .throws_("Cannot resolve 'adhoc.V'; it references view 'adhoc.V', "
+            + "whose definition is cyclic");
+  }
+
+  @Test public void testSelfReferentialView2() throws Exception {
+    final String model = "{\n"
+        + "  version: '1.0',\n"
+        + "  defaultSchema: 'adhoc',\n"
+        + "  schemas: [ {\n"
+        + "    name: 'adhoc',\n"
+        + "    tables: [ {\n"
+        + "      name: 'A',\n"
+        + "      type: 'view',\n"
+        + "      sql: "
+        + new JsonBuilder().toJsonString("select * from B") + "\n"
+        + "    }, {\n"
+        + "      name: 'B',\n"
+        + "      type: 'view',\n"
+        + "      sql: "
+        + new JsonBuilder().toJsonString("select * from C") + "\n"
+        + "    }, {\n"
+        + "      name: 'C',\n"
+        + "      type: 'view',\n"
+        + "      sql: "
+        + new JsonBuilder().toJsonString("select * from D, B") + "\n"
+        + "    }, {\n"
+        + "      name: 'D',\n"
+        + "      type: 'view',\n"
+        + "      sql: "
+        + new JsonBuilder().toJsonString(
+            "select * from (values (1, 'a')) as t(x, y)") + "\n"
+        + "    } ]\n"
+        + "  } ]\n"
+        + "}";
+    final CalciteAssert.AssertThat with =
+        CalciteAssert.model(model);
+    //
+    //       +-----+
+    //       V     |
+    // A --> B --> C --> D
+    //
+    // A is not in a cycle, but depends on cyclic views
+    // B is cyclic
+    // C is cyclic
+    // D is not cyclic
+    with.query("select x from \"adhoc\".a")
+        .throws_("Cannot resolve 'adhoc.A'; it references view 'adhoc.B', "
+            + "whose definition is cyclic");
+    with.query("select x from \"adhoc\".b")
+        .throws_("Cannot resolve 'adhoc.B'; it references view 'adhoc.B', "
+            + "whose definition is cyclic");
+    // as previous, but implicit schema
+    with.query("select x from b")
+        .throws_("Cannot resolve 'B'; it references view 'adhoc.B', "
+            + "whose definition is cyclic");
+    with.query("select x from \"adhoc\".c")
+        .throws_("Cannot resolve 'adhoc.C'; it references view 'adhoc.C', "
+            + "whose definition is cyclic");
+    with.query("select x from \"adhoc\".d")
+        .returns("X=1\n");
+    with.query("select x from \"adhoc\".d except select x from \"adhoc\".a")
+        .throws_("Cannot resolve 'adhoc.A'; it references view 'adhoc.B', "
+            + "whose definition is cyclic");
+  }
+
   /** Tests saving query results into temporary tables, per
    * {@link org.apache.calcite.avatica.Handler.ResultSink}. */
   @Test public void testAutomaticTemporaryTable() throws Exception {

Reply via email to