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"])