mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
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:
72
superset/assets/src/SqlLab/components/ShowSQL.jsx
Normal file
72
superset/assets/src/SqlLab/components/ShowSQL.jsx
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user