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
This commit is contained in:
Beto Dealmeida
2019-09-17 14:24:38 -07:00
committed by GitHub
parent 4132d8fb0f
commit 88777943fa
7 changed files with 170 additions and 21 deletions

View File

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

View File

@@ -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}

View File

@@ -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,

View File

@@ -130,6 +130,13 @@ class BaseEngineSpec:
def get_allow_cost_estimate(cls, version: str = None) -> bool:
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]
@@ -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:

View File

@@ -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):

View File

@@ -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]
]

View File

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