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

beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 8877794  Better distinction between tables and views, and show CREATE 
VIEW (#8213)
8877794 is described below

commit 88777943faf468df7b2c38fc5f5117a5c0ad8cc3
Author: Beto Dealmeida <[email protected]>
AuthorDate: Tue Sep 17 14:24:38 2019 -0700

    Better distinction between tables and views, and show CREATE VIEW (#8213)
    
    * WIP
    
    * Add missing file
    
    * WIP
    
    * Clean up
    
    * Use label instead
    
    * Address comments
    
    * Add docstring
    
    * Fix lint
    
    * Fix typo
    
    * Fix unit test
---
 superset/assets/src/SqlLab/components/ShowSQL.jsx  | 72 ++++++++++++++++++++++
 .../assets/src/SqlLab/components/TableElement.jsx  |  8 +++
 superset/assets/src/components/TableSelector.jsx   | 31 ++++++++++
 superset/db_engine_specs/base.py                   | 12 ++--
 superset/db_engine_specs/presto.py                 | 61 +++++++++++++-----
 superset/views/core.py                             |  6 +-
 tests/db_engine_specs_test.py                      |  1 +
 7 files changed, 170 insertions(+), 21 deletions(-)

diff --git a/superset/assets/src/SqlLab/components/ShowSQL.jsx 
b/superset/assets/src/SqlLab/components/ShowSQL.jsx
new file mode 100644
index 0000000..45da9aa
--- /dev/null
+++ b/superset/assets/src/SqlLab/components/ShowSQL.jsx
@@ -0,0 +1,72 @@
+/**
+ * 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.
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import SyntaxHighlighter, { registerLanguage } from 
'react-syntax-highlighter/dist/light';
+import sql from 'react-syntax-highlighter/dist/languages/hljs/sql';
+import github from 'react-syntax-highlighter/dist/styles/hljs/github';
+
+import { t } from '@superset-ui/translation';
+
+import Link from './Link';
+import ModalTrigger from '../../components/ModalTrigger';
+
+registerLanguage('sql', sql);
+
+const propTypes = {
+  tooltipText: PropTypes.string,
+  title: PropTypes.string,
+  sql: PropTypes.string,
+};
+
+const defaultProps = {
+  tooltipText: t('Show SQL'),
+  title: t('SQL statement'),
+  sql: '',
+};
+
+export default class ShowSQL extends React.PureComponent {
+  renderModalBody() {
+    return (
+      <div>
+        <SyntaxHighlighter language="sql" style={github}>
+          {this.props.sql}
+        </SyntaxHighlighter>
+      </div>
+    );
+  }
+  render() {
+    return (
+      <ModalTrigger
+        modalTitle={this.props.title}
+        triggerNode={
+          <Link
+            className="fa fa-eye pull-left m-l-2"
+            tooltip={this.props.tooltipText}
+            href="#"
+          />
+        }
+        modalBody={this.renderModalBody()}
+      />
+    );
+  }
+}
+
+ShowSQL.propTypes = propTypes;
+ShowSQL.defaultProps = defaultProps;
diff --git a/superset/assets/src/SqlLab/components/TableElement.jsx 
b/superset/assets/src/SqlLab/components/TableElement.jsx
index 403562d..b130c54 100644
--- a/superset/assets/src/SqlLab/components/TableElement.jsx
+++ b/superset/assets/src/SqlLab/components/TableElement.jsx
@@ -25,6 +25,7 @@ import { t } from '@superset-ui/translation';
 import CopyToClipboard from '../../components/CopyToClipboard';
 import Link from './Link';
 import ColumnElement from './ColumnElement';
+import ShowSQL from './ShowSQL';
 import ModalTrigger from '../../components/ModalTrigger';
 import Loading from '../../components/Loading';
 
@@ -172,6 +173,13 @@ class TableElement extends React.PureComponent {
             tooltipText={t('Copy SELECT statement to the clipboard')}
           />
         }
+        {table.view &&
+          <ShowSQL
+            sql={table.view}
+            tooltipText={t('Show CREATE VIEW statement')}
+            title={t('CREATE VIEW statement')}
+          />
+        }
         <Link
           className="fa fa-times table-remove pull-left m-l-2"
           onClick={this.removeTable}
diff --git a/superset/assets/src/components/TableSelector.jsx 
b/superset/assets/src/components/TableSelector.jsx
index bd66eb8..d0785d6 100644
--- a/superset/assets/src/components/TableSelector.jsx
+++ b/superset/assets/src/components/TableSelector.jsx
@@ -110,6 +110,7 @@ export default class TableSelector extends 
React.PureComponent {
         schema: o.schema,
         label: o.label,
         title: o.title,
+        type: o.type,
       }));
       return ({ options });
     });
@@ -140,6 +141,7 @@ export default class TableSelector extends 
React.PureComponent {
             schema: o.schema,
             label: o.label,
             title: o.title,
+            type: o.type,
           }));
           this.setState(() => ({
             tableLoading: false,
@@ -203,6 +205,33 @@ export default class TableSelector extends 
React.PureComponent {
         {db.database_name}
       </span>);
   }
+  renderTableOption({ focusOption, focusedOption, key, option, selectValue, 
style, valueArray }) {
+    const classNames = ['Select-option'];
+    if (option === focusedOption) {
+      classNames.push('is-focused');
+    }
+    if (valueArray.indexOf(option) >= 0) {
+      classNames.push('is-selected');
+    }
+    return (
+      <div
+        className={classNames.join(' ')}
+        key={key}
+        onClick={() => selectValue(option)}
+        onMouseEnter={() => focusOption(option)}
+        style={style}
+      >
+        <span>
+          <span className="m-r-5">
+            <small className="text-muted">
+              <i className={`fa fa-${option.type === 'view' ? 'eye' : 
'table'}`} />
+            </small>
+          </span>
+          {option.label}
+        </span>
+      </div>
+    );
+  }
   renderSelectRow(select, refreshBtn) {
     return (
       <div className="section">
@@ -280,6 +309,7 @@ export default class TableSelector extends 
React.PureComponent {
         onChange={this.changeTable}
         options={options}
         value={this.state.tableName}
+        optionRenderer={this.renderTableOption}
       />) : (
         <Select
           async
@@ -291,6 +321,7 @@ export default class TableSelector extends 
React.PureComponent {
           onChange={this.changeTable}
           value={this.state.tableName}
           loadOptions={this.getTableNamesBySubStr}
+          optionRenderer={this.renderTableOption}
         />);
     return this.renderSelectRow(
       select,
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 3c55427..b71966c 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -131,6 +131,13 @@ class BaseEngineSpec:
         return False
 
     @classmethod
+    def get_engine(cls, database, schema=None, source=None):
+        user_name = utils.get_username()
+        return database.get_sqla_engine(
+            schema=schema, nullpool=True, user_name=user_name, source=source
+        )
+
+    @classmethod
     def get_timestamp_expr(
         cls, col: ColumnClause, pdf: Optional[str], time_grain: Optional[str]
     ) -> TimestampExpression:
@@ -688,10 +695,7 @@ class BaseEngineSpec:
         parsed_query = sql_parse.ParsedQuery(sql)
         statements = parsed_query.get_statements()
 
-        engine = database.get_sqla_engine(
-            schema=schema, nullpool=True, user_name=user_name, source=source
-        )
-
+        engine = cls.get_engine(database, schema=schema, source=source)
         costs = []
         with closing(engine.raw_connection()) as conn:
             with closing(conn.cursor()) as cursor:
diff --git a/superset/db_engine_specs/presto.py 
b/superset/db_engine_specs/presto.py
index 345d5a4..c060da5 100644
--- a/superset/db_engine_specs/presto.py
+++ b/superset/db_engine_specs/presto.py
@@ -16,13 +16,14 @@
 # under the License.
 # pylint: disable=C,R,W
 from collections import defaultdict, deque, OrderedDict
+from contextlib import closing
 from datetime import datetime
 from distutils.version import StrictVersion
 import logging
 import re
 import textwrap
 import time
-from typing import Any, Dict, List, Optional, Set, Tuple
+from typing import Any, cast, Dict, List, Optional, Set, Tuple
 from urllib import parse
 
 import simplejson as json
@@ -971,25 +972,55 @@ class PrestoEngineSpec(BaseEngineSpec):
     def extra_table_metadata(
         cls, database, table_name: str, schema_name: str
     ) -> Dict[str, Any]:
+        metadata = {}
+
         indexes = database.get_indexes(table_name, schema_name)
-        if not indexes:
-            return {}
-        cols = indexes[0].get("column_names", [])
-        full_table_name = table_name
-        if schema_name and "." not in table_name:
-            full_table_name = "{}.{}".format(schema_name, table_name)
-        pql = cls._partition_query(full_table_name, database)
-        col_names, latest_parts = cls.latest_partition(
-            table_name, schema_name, database, show_first=True
-        )
-        latest_parts = latest_parts or tuple([None] * len(col_names))
-        return {
-            "partitions": {
+        if indexes:
+            cols = indexes[0].get("column_names", [])
+            full_table_name = table_name
+            if schema_name and "." not in table_name:
+                full_table_name = "{}.{}".format(schema_name, table_name)
+            pql = cls._partition_query(full_table_name, database)
+            col_names, latest_parts = cls.latest_partition(
+                table_name, schema_name, database, show_first=True
+            )
+            latest_parts = latest_parts or tuple([None] * len(col_names))
+            metadata["partitions"] = {
                 "cols": cols,
                 "latest": dict(zip(col_names, latest_parts)),
                 "partitionQuery": pql,
             }
-        }
+
+        # flake8 is not matching `Optional[str]` to `Any` for some reason...
+        metadata["view"] = cast(
+            Any, cls.get_create_view(database, schema_name, table_name)
+        )
+
+        return metadata
+
+    @classmethod
+    def get_create_view(cls, database, schema: str, table: str) -> 
Optional[str]:
+        """
+        Return a CREATE VIEW statement, or `None` if not a view.
+
+        :param database: Database instance
+        :param schema: Schema name
+        :param table: Table (view) name
+        """
+        engine = cls.get_engine(database, schema)
+        with closing(engine.raw_connection()) as conn:
+            with closing(conn.cursor()) as cursor:
+                sql = f"SHOW CREATE VIEW {schema}.{table}"
+                cls.execute(cursor, sql)
+                try:
+                    polled = cursor.poll()
+                except Exception:  # not a VIEW
+                    return None
+                while polled:
+                    time.sleep(0.2)
+                    polled = cursor.poll()
+                rows = cls.fetch_data(cursor, 1)
+        return rows[0][0]
 
     @classmethod
     def handle_cursor(cls, cursor, query, session):
diff --git a/superset/views/core.py b/superset/views/core.py
index 826f04c..36fb3ca 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1556,6 +1556,7 @@ class Superset(BaseSupersetView):
                 "schema": tn.schema,
                 "label": get_datasource_label(tn),
                 "title": get_datasource_label(tn),
+                "type": "table",
             }
             for tn in tables[:max_tables]
         ]
@@ -1564,8 +1565,9 @@ class Superset(BaseSupersetView):
                 {
                     "value": vn.table,
                     "schema": vn.schema,
-                    "label": f"[view] {get_datasource_label(vn)}",
-                    "title": f"[view] {get_datasource_label(vn)}",
+                    "label": get_datasource_label(vn),
+                    "title": get_datasource_label(vn),
+                    "type": "view",
                 }
                 for vn in views[:max_views]
             ]
diff --git a/tests/db_engine_specs_test.py b/tests/db_engine_specs_test.py
index 2c6e3b3..f6354c0 100644
--- a/tests/db_engine_specs_test.py
+++ b/tests/db_engine_specs_test.py
@@ -859,6 +859,7 @@ class DbEngineSpecsTestCase(SupersetTestCase):
         db.get_extra = mock.Mock(return_value={})
         df = pd.DataFrame({"ds": ["01-01-19"], "hour": [1]})
         db.get_df = mock.Mock(return_value=df)
+        PrestoEngineSpec.get_create_view = mock.Mock(return_value=None)
         result = PrestoEngineSpec.extra_table_metadata(db, "test_table", 
"test_schema")
         self.assertEqual({"ds": "01-01-19", "hour": 1}, 
result["partitions"]["latest"])
 

Reply via email to