mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
[sqllab] show partition metadata for Presto (#1342)
This commit is contained in:
committed by
GitHub
parent
2edce5bf8a
commit
c255e89219
@@ -2,7 +2,7 @@ export const RESET_STATE = 'RESET_STATE';
|
||||
export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
|
||||
export const CLONE_QUERY_TO_NEW_TAB = 'CLONE_QUERY_TO_NEW_TAB';
|
||||
export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR';
|
||||
export const ADD_TABLE = 'ADD_TABLE';
|
||||
export const MERGE_TABLE = 'MERGE_TABLE';
|
||||
export const REMOVE_TABLE = 'REMOVE_TABLE';
|
||||
export const START_QUERY = 'START_QUERY';
|
||||
export const STOP_QUERY = 'STOP_QUERY';
|
||||
@@ -86,8 +86,8 @@ export function queryEditorSetSql(queryEditor, sql) {
|
||||
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
|
||||
}
|
||||
|
||||
export function addTable(table) {
|
||||
return { type: ADD_TABLE, table };
|
||||
export function mergeTable(table) {
|
||||
return { type: MERGE_TABLE, table };
|
||||
}
|
||||
|
||||
export function expandTable(table) {
|
||||
|
||||
@@ -55,7 +55,7 @@ class SouthPane extends React.Component {
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs bsStyle="tabs" id={shortid.generate()}>
|
||||
<Tab title="Results" eventKey={1} id={shortid.generate()}>
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ import shortid from 'shortid';
|
||||
import SouthPane from './SouthPane';
|
||||
import Timer from './Timer';
|
||||
|
||||
import SqlEditorTopToolbar from './SqlEditorTopToolbar';
|
||||
import SqlEditorLeftBar from './SqlEditorLeftBar';
|
||||
|
||||
class SqlEditor extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -252,7 +252,7 @@ class SqlEditor extends React.Component {
|
||||
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<SqlEditorTopToolbar queryEditor={this.props.queryEditor} />
|
||||
<SqlEditorLeftBar queryEditor={this.props.queryEditor} />
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<AceEditor
|
||||
|
||||
@@ -4,14 +4,13 @@ import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import Select from 'react-select';
|
||||
import { Label, Button } from 'react-bootstrap';
|
||||
import TableElement from './TableElement';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
|
||||
|
||||
class SqlEditorTopToolbar extends React.Component {
|
||||
class SqlEditorLeftBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -79,12 +78,11 @@ class SqlEditorTopToolbar extends React.Component {
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const qe = this.props.queryEditor;
|
||||
const url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
let url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
|
||||
this.setState({ tableLoading: true });
|
||||
$.get(url, (data) => {
|
||||
this.props.actions.addTable({
|
||||
id: shortid.generate(),
|
||||
this.props.actions.mergeTable({
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
name: data.name,
|
||||
@@ -102,6 +100,18 @@ class SqlEditorTopToolbar extends React.Component {
|
||||
});
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
|
||||
url = `/caravel/extra_table_metadata/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
const table = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
schema: qe.schema,
|
||||
name: tableName,
|
||||
};
|
||||
Object.assign(table, data);
|
||||
this.props.actions.mergeTable(table);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
let networkAlert = null;
|
||||
@@ -158,14 +168,14 @@ class SqlEditorTopToolbar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditorTopToolbar.propTypes = {
|
||||
SqlEditorLeftBar.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
SqlEditorTopToolbar.defaultProps = {
|
||||
SqlEditorLeftBar.defaultProps = {
|
||||
tables: [],
|
||||
};
|
||||
|
||||
@@ -182,4 +192,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorTopToolbar);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorLeftBar);
|
||||
@@ -116,7 +116,6 @@ class TabbedSqlEditors extends React.Component {
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
id={`a11y-query-editor-${qe.id}`}
|
||||
>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import React from 'react';
|
||||
import { ButtonGroup } from 'react-bootstrap';
|
||||
import { ButtonGroup, Well } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import ModalTrigger from '../../components/ModalTrigger.jsx';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
|
||||
const propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
table: null,
|
||||
actions: {},
|
||||
};
|
||||
|
||||
class TableElement extends React.Component {
|
||||
setSelectStar() {
|
||||
@@ -40,40 +52,85 @@ class TableElement extends React.Component {
|
||||
|
||||
collapseTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.collapseTable.bind(this, this.props.table)();
|
||||
this.props.actions.collapseTable(this.props.table);
|
||||
}
|
||||
|
||||
expandTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.expandTable.bind(this, this.props.table)();
|
||||
this.props.actions.expandTable(this.props.table);
|
||||
}
|
||||
|
||||
removeTable() {
|
||||
this.props.actions.removeTable(this.props.table);
|
||||
}
|
||||
|
||||
render() {
|
||||
const table = this.props.table;
|
||||
let metadata = null;
|
||||
let buttonToggle;
|
||||
if (this.props.table.expanded) {
|
||||
|
||||
let header;
|
||||
if (table.partitions) {
|
||||
let partitionQuery;
|
||||
let partitionClipBoard;
|
||||
if (table.partitions.partitionQuery) {
|
||||
partitionQuery = table.partitions.partitionQuery;
|
||||
const tt = 'Copy partition query to clipboard';
|
||||
partitionClipBoard = (
|
||||
<CopyToClipboard
|
||||
text={partitionQuery}
|
||||
shouldShowText={false}
|
||||
tooltipText={tt}
|
||||
copyNode={<i className="fa fa-clipboard" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let latest = [];
|
||||
for (const k in table.partitions.latest) {
|
||||
latest.push(`${k}=${table.partitions.latest[k]}`);
|
||||
}
|
||||
latest = latest.join('/');
|
||||
header = (
|
||||
<Well bsSize="small">
|
||||
<div>
|
||||
<small>
|
||||
latest partition: {latest}
|
||||
</small> {partitionClipBoard}
|
||||
</div>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
if (table.expanded) {
|
||||
buttonToggle = (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { this.collapseTable(e); }}
|
||||
>
|
||||
<strong>{this.props.table.name}</strong>
|
||||
<strong>{table.name}</strong>
|
||||
<small className="m-l-5"><i className="fa fa-minus" /></small>
|
||||
</a>
|
||||
);
|
||||
metadata = (
|
||||
<div>
|
||||
{this.props.table.columns.map((col) => (
|
||||
<div className="clearfix" key={shortid.generate()}>
|
||||
<div className="pull-left m-l-10">
|
||||
{col.name}
|
||||
</div>
|
||||
<div className="pull-right text-muted">
|
||||
<small> {col.type}</small>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{header}
|
||||
<div className="table-columns">
|
||||
{table.columns.map((col) => {
|
||||
let name = col.name;
|
||||
if (col.indexed) {
|
||||
name = <strong>{col.name}</strong>;
|
||||
}
|
||||
return (
|
||||
<div className="clearfix table-column" key={shortid.generate()}>
|
||||
<div className="pull-left m-l-10">
|
||||
{name}
|
||||
</div>
|
||||
<div className="pull-right text-muted">
|
||||
<small> {col.type}</small>
|
||||
</div>
|
||||
</div>);
|
||||
})}
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@@ -82,34 +139,34 @@ class TableElement extends React.Component {
|
||||
href="#"
|
||||
onClick={(e) => { this.expandTable(e); }}
|
||||
>
|
||||
{this.props.table.name}
|
||||
{table.name}
|
||||
<small className="m-l-5"><i className="fa fa-plus" /></small>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
let keyLink;
|
||||
if (this.props.table.indexes && this.props.table.indexes.length > 0) {
|
||||
if (table.indexes && table.indexes.length > 0) {
|
||||
keyLink = (
|
||||
<ModalTrigger
|
||||
modalTitle={
|
||||
<div>
|
||||
Keys for table <strong>{this.props.table.name}</strong>
|
||||
Keys for table <strong>{table.name}</strong>
|
||||
</div>
|
||||
}
|
||||
modalBody={
|
||||
<pre>{JSON.stringify(this.props.table.indexes, null, 4)}</pre>
|
||||
<pre>{JSON.stringify(table.indexes, null, 4)}</pre>
|
||||
}
|
||||
triggerNode={
|
||||
<Link
|
||||
className="fa fa-key pull-left m-l-2"
|
||||
tooltip={`View indexes (${this.props.table.indexes.length})`}
|
||||
tooltip={`View indexes (${table.indexes.length})`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="TableElement">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
{buttonToggle}
|
||||
@@ -131,26 +188,22 @@ class TableElement extends React.Component {
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash pull-left m-l-2"
|
||||
onClick={this.props.actions.removeTable.bind(this, this.props.table)}
|
||||
onClick={this.removeTable.bind(this)}
|
||||
tooltip="Remove from workspace"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
{metadata}
|
||||
<div>
|
||||
{metadata}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableElement.propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
TableElement.defaultProps = {
|
||||
table: null,
|
||||
};
|
||||
TableElement.propTypes = propTypes;
|
||||
TableElement.defaultProps = defaultProps;
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
@@ -158,3 +211,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(TableElement);
|
||||
export { TableElement };
|
||||
|
||||
@@ -232,3 +232,9 @@ div.tablePopover:hover {
|
||||
.SouthPane .tab-content {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.TableElement .well {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@@ -72,8 +72,23 @@ export const sqlLabReducer = function (state, action) {
|
||||
[actions.RESET_STATE]() {
|
||||
return Object.assign({}, initialState);
|
||||
},
|
||||
[actions.ADD_TABLE]() {
|
||||
return addToArr(state, 'tables', action.table);
|
||||
[actions.MERGE_TABLE]() {
|
||||
const at = Object.assign({}, action.table);
|
||||
let existingTable;
|
||||
state.tables.forEach((t) => {
|
||||
if (
|
||||
t.dbId === at.dbId &&
|
||||
t.queryEditorId === at.queryEditorId &&
|
||||
t.schema === at.schema &&
|
||||
t.name === at.name) {
|
||||
existingTable = t;
|
||||
}
|
||||
});
|
||||
if (existingTable) {
|
||||
return alterInArr(state, 'tables', existingTable, at);
|
||||
}
|
||||
at.id = shortid.generate();
|
||||
return addToArr(state, 'tables', at);
|
||||
},
|
||||
[actions.EXPAND_TABLE]() {
|
||||
return alterInArr(state, 'tables', action.table, { expanded: true });
|
||||
|
||||
@@ -76,9 +76,11 @@ export default class CopyToClipboard extends React.Component {
|
||||
return (
|
||||
<span>
|
||||
{this.props.shouldShowText &&
|
||||
<span>{this.props.text}</span>
|
||||
<span>
|
||||
{this.props.text}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
<OverlayTrigger placement="top" overlay={this.renderTooltip()} trigger={['hover']}>
|
||||
<Button
|
||||
bsStyle="link"
|
||||
|
||||
@@ -4,7 +4,7 @@ import cx from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
modalTitle: PropTypes.node.isRequired,
|
||||
modalBody: PropTypes.node.isRequired,
|
||||
beforeOpen: PropTypes.func,
|
||||
isButton: PropTypes.bool,
|
||||
|
||||
158
caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
Normal file
158
caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import Link from '../../../javascripts/SqlLab/components/Link';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { TableElement } from '../../../javascripts/SqlLab/components/TableElement';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe('TableElement', () => {
|
||||
|
||||
const mockedProps = {
|
||||
'table': {
|
||||
"dbId": 1,
|
||||
"queryEditorId": "rJ-KP47a",
|
||||
"schema": "caravel",
|
||||
"name": "ab_user",
|
||||
"id": "r11Vgt60",
|
||||
"indexes": [
|
||||
{
|
||||
"unique": true,
|
||||
"column_names": [
|
||||
"username"
|
||||
],
|
||||
"type": "UNIQUE",
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"unique": true,
|
||||
"column_names": [
|
||||
"email"
|
||||
],
|
||||
"type": "UNIQUE",
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"unique": false,
|
||||
"column_names": [
|
||||
"created_by_fk"
|
||||
],
|
||||
"name": "created_by_fk"
|
||||
},
|
||||
{
|
||||
"unique": false,
|
||||
"column_names": [
|
||||
"changed_by_fk"
|
||||
],
|
||||
"name": "changed_by_fk"
|
||||
}
|
||||
],
|
||||
"columns": [
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "first_name"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "last_name"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(256)",
|
||||
"type": "VARCHAR",
|
||||
"name": "password"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "TINYINT(1)",
|
||||
"type": "TINYINT",
|
||||
"name": "active"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "last_login"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "login_count"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "fail_login_count"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "created_on"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "changed_on"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "created_by_fk"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "changed_by_fk"
|
||||
}
|
||||
],
|
||||
"expanded": true
|
||||
}
|
||||
}
|
||||
it('should just render', () => {
|
||||
expect(
|
||||
React.isValidElement(<TableElement />)
|
||||
).to.equal(true);
|
||||
});
|
||||
it('should render with props', () => {
|
||||
expect(
|
||||
React.isValidElement(<TableElement {...mockedProps} />)
|
||||
).to.equal(true);
|
||||
});
|
||||
it('has 3 Link elements', () => {
|
||||
const wrapper = shallow(<TableElement {...mockedProps} />);
|
||||
expect(wrapper.find(Link)).to.have.length(3);
|
||||
});
|
||||
it('has 14 columns', () => {
|
||||
const wrapper = shallow(<TableElement {...mockedProps} />);
|
||||
expect(wrapper.find('div.table-column')).to.have.length(14);
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,10 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import inspect
|
||||
from collections import namedtuple
|
||||
import inspect
|
||||
import textwrap
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
Grain = namedtuple('Grain', 'name label function')
|
||||
@@ -36,7 +38,7 @@ class BaseEngineSpec(object):
|
||||
return cls.epoch_to_dttm().replace('{col}', '({col}/1000.0)')
|
||||
|
||||
@classmethod
|
||||
def extra_table_metadata(cls, table):
|
||||
def extra_table_metadata(cls, database, table_name, schema_name):
|
||||
"""Returns engine-specific table metadata"""
|
||||
return {}
|
||||
|
||||
@@ -109,6 +111,7 @@ class MySQLEngineSpec(BaseEngineSpec):
|
||||
Grain("month", _('month'), "DATE(DATE_SUB({col}, "
|
||||
"INTERVAL DAYOFMONTH({col}) - 1 DAY))"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def convert_dttm(cls, target_type, dttm):
|
||||
if target_type.upper() in ('DATETIME', 'DATE'):
|
||||
@@ -158,6 +161,46 @@ class PrestoEngineSpec(BaseEngineSpec):
|
||||
def epoch_to_dttm(cls):
|
||||
return "from_unixtime({col})"
|
||||
|
||||
@staticmethod
|
||||
def show_partition_pql(
|
||||
table_name, schema_name=None, order_by=None, limit=100):
|
||||
if schema_name:
|
||||
table_name = schema_name + '.' + table_name
|
||||
order_by = order_by or []
|
||||
order_by_clause = ''
|
||||
if order_by:
|
||||
order_by_clause = "ORDER BY " + ', '.join(order_by) + " DESC"
|
||||
|
||||
limit_clause = ''
|
||||
if limit:
|
||||
limit_clause = "LIMIT {}".format(limit)
|
||||
|
||||
return textwrap.dedent("""\
|
||||
SHOW PARTITIONS
|
||||
FROM {table_name}
|
||||
{order_by_clause}
|
||||
{limit_clause}
|
||||
""").format(**locals())
|
||||
|
||||
@classmethod
|
||||
def extra_table_metadata(cls, database, table_name, schema_name):
|
||||
indexes = database.get_indexes(table_name, schema_name)
|
||||
if not indexes:
|
||||
return {}
|
||||
cols = indexes[0].get('column_names', [])
|
||||
pql = cls.show_partition_pql(table_name, schema_name, cols)
|
||||
df = database.get_df(pql, schema_name)
|
||||
latest_part = df.to_dict(orient='records')[0] if not df.empty else None
|
||||
|
||||
partition_query = cls.show_partition_pql(table_name, schema_name, cols)
|
||||
return {
|
||||
'partitions': {
|
||||
'cols': cols,
|
||||
'latest': latest_part,
|
||||
'partitionQuery': partition_query,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MssqlEngineSpec(BaseEngineSpec):
|
||||
engine = 'mssql'
|
||||
@@ -189,7 +232,7 @@ class MssqlEngineSpec(BaseEngineSpec):
|
||||
|
||||
@classmethod
|
||||
def convert_dttm(cls, target_type, dttm):
|
||||
return "CONVERT(DATETIME, '{}', 126)".format(iso)
|
||||
return "CONVERT(DATETIME, '{}', 126)".format(dttm.isoformat())
|
||||
|
||||
|
||||
class RedshiftEngineSpec(PostgresEngineSpec):
|
||||
|
||||
@@ -1895,6 +1895,10 @@ class Caravel(BaseCaravelView):
|
||||
return Response(
|
||||
json.dumps({'error': utils.error_msg_from_exception(e)}),
|
||||
mimetype="application/json")
|
||||
indexed_columns = set()
|
||||
for index in indexes:
|
||||
indexed_columns |= set(index.get('column_names', []))
|
||||
|
||||
for col in t:
|
||||
dtype = ""
|
||||
try:
|
||||
@@ -1905,6 +1909,7 @@ class Caravel(BaseCaravelView):
|
||||
'name': col['name'],
|
||||
'type': dtype.split('(')[0] if '(' in dtype else dtype,
|
||||
'longType': dtype,
|
||||
'indexed': col['name'] in indexed_columns,
|
||||
})
|
||||
tbl = {
|
||||
'name': table_name,
|
||||
@@ -1913,6 +1918,16 @@ class Caravel(BaseCaravelView):
|
||||
}
|
||||
return Response(json.dumps(tbl), mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/extra_table_metadata/<database_id>/<table_name>/<schema>/")
|
||||
@log_this
|
||||
def extra_table_metadata(self, database_id, table_name, schema):
|
||||
schema = None if schema in ('null', 'undefined') else schema
|
||||
mydb = db.session.query(models.Database).filter_by(id=database_id).one()
|
||||
payload = mydb.db_engine_spec.extra_table_metadata(
|
||||
mydb, table_name, schema)
|
||||
return Response(json.dumps(payload), mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/select_star/<database_id>/<table_name>/")
|
||||
@log_this
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import imp
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
@@ -124,6 +125,18 @@ class CaravelTestCase(unittest.TestCase):
|
||||
resp = self.client.get(url, follow_redirects=True)
|
||||
return resp.data.decode('utf-8')
|
||||
|
||||
def get_json_resp(self, url):
|
||||
"""Shortcut to get the parsed results while following redirects"""
|
||||
resp = self.get_resp(url)
|
||||
return json.loads(resp)
|
||||
|
||||
def get_main_database(self, session):
|
||||
return (
|
||||
db.session.query(models.Database)
|
||||
.filter_by(database_name='main')
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_access_requests(self, username, ds_type, ds_id):
|
||||
DAR = models.DatasourceAccessRequest
|
||||
return (
|
||||
|
||||
@@ -15,10 +15,7 @@ import unittest
|
||||
from flask import escape
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
|
||||
import caravel
|
||||
from caravel import app, db, models, utils, appbuilder, sm
|
||||
from caravel.source_registry import SourceRegistry
|
||||
from caravel.models import DruidDatasource
|
||||
from caravel import db, models, utils, appbuilder, sm
|
||||
from caravel.views import DatabaseView
|
||||
|
||||
from .base_tests import CaravelTestCase
|
||||
@@ -156,12 +153,7 @@ class CoreTests(CaravelTestCase):
|
||||
assert self.get_resp('/ping') == "OK"
|
||||
|
||||
def test_testconn(self):
|
||||
database = (
|
||||
db.session
|
||||
.query(models.Database)
|
||||
.filter_by(database_name='main')
|
||||
.first()
|
||||
)
|
||||
database = self.get_main_database(db.session)
|
||||
|
||||
# validate that the endpoint works with the password-masked sqlalchemy uri
|
||||
data = json.dumps({
|
||||
@@ -182,25 +174,23 @@ class CoreTests(CaravelTestCase):
|
||||
def test_databaseview_edit(self, username='admin'):
|
||||
# validate that sending a password-masked uri does not over-write the decrypted uri
|
||||
self.login(username=username)
|
||||
database = db.session.query(models.Database).filter_by(database_name='main').first()
|
||||
database = self.get_main_database(db.session)
|
||||
sqlalchemy_uri_decrypted = database.sqlalchemy_uri_decrypted
|
||||
url = 'databaseview/edit/{}'.format(database.id)
|
||||
data = {k: database.__getattribute__(k) for k in DatabaseView.add_columns}
|
||||
data['sqlalchemy_uri'] = database.safe_sqlalchemy_uri()
|
||||
response = self.client.post(url, data=data)
|
||||
database = db.session.query(models.Database).filter_by(database_name='main').first()
|
||||
self.client.post(url, data=data)
|
||||
database = self.get_main_database(db.session)
|
||||
self.assertEqual(sqlalchemy_uri_decrypted, database.sqlalchemy_uri_decrypted)
|
||||
|
||||
def test_warm_up_cache(self):
|
||||
slice = db.session.query(models.Slice).first()
|
||||
resp = self.get_resp(
|
||||
data = self.get_json_resp(
|
||||
'/caravel/warm_up_cache?slice_id={}'.format(slice.id))
|
||||
data = json.loads(resp)
|
||||
assert data == [{'slice_id': slice.id, 'slice_name': slice.slice_name}]
|
||||
|
||||
resp = self.get_resp(
|
||||
data = self.get_json_resp(
|
||||
'/caravel/warm_up_cache?table_name=energy_usage&db_name=main')
|
||||
data = json.loads(resp)
|
||||
assert len(data) == 3
|
||||
|
||||
def test_shortner(self):
|
||||
@@ -275,11 +265,7 @@ class CoreTests(CaravelTestCase):
|
||||
|
||||
def run_sql(self, sql, user_name, client_id):
|
||||
self.login(username=user_name)
|
||||
dbid = (
|
||||
db.session.query(models.Database)
|
||||
.filter_by(database_name='main')
|
||||
.first().id
|
||||
)
|
||||
dbid = self.get_main_database(db.session).id
|
||||
resp = self.client.post(
|
||||
'/caravel/sql_json/',
|
||||
data=dict(database_id=dbid, sql=sql, select_as_create_as=False,
|
||||
@@ -296,9 +282,7 @@ class CoreTests(CaravelTestCase):
|
||||
assert len(data['error']) > 0
|
||||
|
||||
def test_sql_json_has_access(self):
|
||||
main_db = (
|
||||
db.session.query(models.Database).filter_by(database_name="main").first()
|
||||
)
|
||||
main_db = self.get_main_database(db.session)
|
||||
utils.merge_perm(sm, 'database_access', main_db.perm)
|
||||
db.session.commit()
|
||||
main_db_permission_view = (
|
||||
@@ -347,16 +331,14 @@ class CoreTests(CaravelTestCase):
|
||||
self.assertEquals(403, resp.status_code)
|
||||
|
||||
self.login('admin')
|
||||
resp = self.get_resp('/caravel/queries/{}'.format(0))
|
||||
data = json.loads(resp)
|
||||
data = self.get_json_resp('/caravel/queries/{}'.format(0))
|
||||
self.assertEquals(0, len(data))
|
||||
self.logout()
|
||||
|
||||
self.run_sql("SELECT * FROM ab_user", 'admin', client_id='client_id_1')
|
||||
self.run_sql("SELECT * FROM ab_user1", 'admin', client_id='client_id_2')
|
||||
self.login('admin')
|
||||
resp = self.get_resp('/caravel/queries/{}'.format(0))
|
||||
data = json.loads(resp)
|
||||
data = self.get_json_resp('/caravel/queries/{}'.format(0))
|
||||
self.assertEquals(2, len(data))
|
||||
|
||||
query = db.session.query(models.Query).filter_by(
|
||||
@@ -364,8 +346,7 @@ class CoreTests(CaravelTestCase):
|
||||
query.changed_on = utils.EPOCH
|
||||
db.session.commit()
|
||||
|
||||
resp = self.get_resp('/caravel/queries/{}'.format(123456000))
|
||||
data = json.loads(resp)
|
||||
data = self.get_json_resp('/caravel/queries/{}'.format(123456000))
|
||||
self.assertEquals(1, len(data))
|
||||
|
||||
self.logout()
|
||||
@@ -450,5 +431,12 @@ class CoreTests(CaravelTestCase):
|
||||
db.session.commit()
|
||||
self.test_save_dash('alpha')
|
||||
|
||||
def test_extra_table_metadata(self):
|
||||
self.login('admin')
|
||||
dbid = self.get_main_database(db.session).id
|
||||
self.get_json_resp(
|
||||
'/caravel/extra_table_metadata/{dbid}/'
|
||||
'ab_permission_view/panoramix/'.format(**locals()))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user