Skip to content

Commit

Permalink
[sqllab] surfacing more table metadata (indices, pk, fks) (#1485)
Browse files Browse the repository at this point in the history
* Expose more table/column metadata

* [sqllab] expose more table metadata

* more tests
  • Loading branch information
mistercrunch authored Nov 1, 2016
1 parent 76499af commit 61509bb
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 28 deletions.
59 changes: 59 additions & 0 deletions caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';

import { OverlayTrigger, Tooltip } from 'react-bootstrap';

const propTypes = {
column: React.PropTypes.object.isRequired,
};

const iconMap = {
pk: 'fa-key',
fk: 'fa-link',
index: 'fa-bookmark',
};
const tooltipTitleMap = {
pk: 'Primary Key',
fk: 'Foreign Key',
index: 'Index',
};

class ColumnElement extends React.PureComponent {
render() {
const col = this.props.column;
let name = col.name;
let icons;
if (col.keys && col.keys.length > 0) {
name = <strong>{col.name}</strong>;
icons = col.keys.map((key, i) => (
<span key={i} className="ColumnElement">
<OverlayTrigger
placement="right"
overlay={
<Tooltip id="idx-json" bsSize="lg">
<strong>{tooltipTitleMap[key.type]}</strong>
<hr />
<pre className="text-small">
{JSON.stringify(key, null, ' ')}
</pre>
</Tooltip>
}
>
<i className={`fa text-muted m-l-2 ${iconMap[key.type]}`} />
</OverlayTrigger>
</span>
));
}
return (
<div className="clearfix table-column">
<div className="pull-left m-l-10 col-name">
{name}{icons}
</div>
<div className="pull-right text-muted">
<small> {col.type}</small>
</div>
</div>);
}
}
ColumnElement.propTypes = propTypes;

export default ColumnElement;
28 changes: 8 additions & 20 deletions caravel/assets/javascripts/SqlLab/components/TableElement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import shortid from 'shortid';

import CopyToClipboard from '../../components/CopyToClipboard';
import Link from './Link';
import ColumnElement from './ColumnElement';
import ModalTrigger from '../../components/ModalTrigger';

const propTypes = {
Expand Down Expand Up @@ -119,21 +120,9 @@ class TableElement extends React.PureComponent {
<div>
{this.renderHeader()}
<div className="table-columns">
{cols && cols.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 col-name">
{name}
</div>
<div className="pull-right text-muted">
<small> {col.type}</small>
</div>
</div>);
})}
{cols && cols.map(col => (
<ColumnElement column={col} key={col.name} />
))}
<hr />
</div>
</div>
Expand All @@ -147,7 +136,6 @@ class TableElement extends React.PureComponent {

render() {
const table = this.props.table;

let keyLink;
if (table.indexes && table.indexes.length > 0) {
keyLink = (
Expand All @@ -157,13 +145,13 @@ class TableElement extends React.PureComponent {
Keys for table <strong>{table.name}</strong>
</div>
}
modalBody={
<pre>{JSON.stringify(table.indexes, null, 4)}</pre>
}
modalBody={table.indexes.map((ix, i) => (
<pre key={i}>{JSON.stringify(ix, null, ' ')}</pre>
))}
triggerNode={
<Link
className="fa fa-key pull-left m-l-2"
tooltip={`View indexes (${table.indexes.length})`}
tooltip={`View keys & indexes (${table.indexes.length})`}
/>
}
/>
Expand Down
10 changes: 10 additions & 0 deletions caravel/assets/javascripts/SqlLab/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,13 @@ a.Link {
padding: 3px 5px;
margin: 3px 5px;
}
.tooltip pre {
background: transparent;
border: none;
text-align: left;
color: white;
font-size: 10px;
}
.tooltip-inner {
max-width: 500px;
}
35 changes: 35 additions & 0 deletions caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import ColumnElement from '../../../javascripts/SqlLab/components/ColumnElement';
import { mockedActions, table } from './fixtures';
import { mount } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';


describe('ColumnElement', () => {
const mockedProps = {
actions: mockedActions,
column: table.columns[0],
};
it('is valid with props', () => {
expect(
React.isValidElement(<ColumnElement {...mockedProps} />)
).to.equal(true);
});
it('renders a proper primary key', () => {
const wrapper = mount(<ColumnElement column={table.columns[0]} />);
expect(wrapper.find('i.fa-key')).to.have.length(1);
expect(wrapper.find('.col-name').first().text()).to.equal('id');
});
it('renders a multi-key column', () => {
const wrapper = mount(<ColumnElement column={table.columns[1]} />);
expect(wrapper.find('i.fa-link')).to.have.length(1);
expect(wrapper.find('i.fa-bookmark')).to.have.length(1);
expect(wrapper.find('.col-name').first().text()).to.equal('first_name');
});
it('renders a column with no keys', () => {
const wrapper = mount(<ColumnElement column={table.columns[2]} />);
expect(wrapper.find('i')).to.have.length(0);
expect(wrapper.find('.col-name').first().text()).to.equal('last_name');
});
});
7 changes: 4 additions & 3 deletions caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import Link from '../../../javascripts/SqlLab/components/Link';
import TableElement from '../../../javascripts/SqlLab/components/TableElement';
import ColumnElement from '../../../javascripts/SqlLab/components/ColumnElement';
import { mockedActions, table } from './fixtures';
import { mount, shallow } from 'enzyme';
import { describe, it } from 'mocha';
Expand Down Expand Up @@ -29,18 +30,18 @@ describe('TableElement', () => {
});
it('has 14 columns', () => {
const wrapper = shallow(<TableElement {...mockedProps} />);
expect(wrapper.find('div.table-column')).to.have.length(14);
expect(wrapper.find(ColumnElement)).to.have.length(14);
});
it('mounts', () => {
mount(<TableElement {...mockedProps} />);
});
it('sorts columns', () => {
const wrapper = mount(<TableElement {...mockedProps} />);
expect(wrapper.state().sortColumns).to.equal(false);
expect(wrapper.find('.col-name').first().text()).to.equal('id');
expect(wrapper.find(ColumnElement).first().props().column.name).to.equal('id');
wrapper.find('.sort-cols').simulate('click');
expect(wrapper.state().sortColumns).to.equal(true);
expect(wrapper.find('.col-name').first().text()).to.equal('last_login');
expect(wrapper.find(ColumnElement).first().props().column.name).to.equal('last_login');
});
it('calls the collapseTable action', () => {
const wrapper = mount(<TableElement {...mockedProps} />);
Expand Down
30 changes: 30 additions & 0 deletions caravel/assets/spec/javascripts/sqllab/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,42 @@ export const table = {
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'id',
keys: [
{
column_names: ['id'],
type: 'pk',
name: null,
},
],
},
{
indexed: false,
longType: 'VARCHAR(64)',
type: 'VARCHAR',
name: 'first_name',
keys: [
{
column_names: [
'first_name',
],
name: 'slices_ibfk_1',
referred_columns: [
'id',
],
referred_table: 'datasources',
type: 'fk',
referred_schema: 'carapal',
options: {},
},
{
unique: false,
column_names: [
'druid_datasource_id',
],
type: 'index',
name: 'druid_datasource_id',
},
],
},
{
indexed: false,
Expand Down
6 changes: 6 additions & 0 deletions caravel/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,12 @@ def get_columns(self, table_name, schema=None):
def get_indexes(self, table_name, schema=None):
return self.inspector.get_indexes(table_name, schema)

def get_pk_constraint(self, table_name, schema=None):
return self.inspector.get_pk_constraint(table_name, schema)

def get_foreign_keys(self, table_name, schema=None):
return self.inspector.get_foreign_keys(table_name, schema)

@property
def sqlalchemy_uri_decrypted(self):
conn = sqla.engine.url.make_url(self.sqlalchemy_uri)
Expand Down
26 changes: 21 additions & 5 deletions caravel/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1947,13 +1947,24 @@ def table(self, database_id, table_name, schema):
try:
t = mydb.get_columns(table_name, schema)
indexes = mydb.get_indexes(table_name, schema)
primary_key = mydb.get_pk_constraint(table_name, schema)
foreign_keys = mydb.get_foreign_keys(table_name, schema)
except Exception as e:
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', []))
keys = []
if primary_key and primary_key.get('constrained_columns'):
primary_key['column_names'] = primary_key.pop('constrained_columns')
primary_key['type'] = 'pk'
keys += [primary_key]
for fk in foreign_keys:
fk['column_names'] = fk.pop('constrained_columns')
fk['type'] = 'fk'
keys += foreign_keys
for idx in indexes:
idx['type'] = 'index'
keys += indexes

for col in t:
dtype = ""
Expand All @@ -1965,14 +1976,19 @@ def table(self, database_id, table_name, schema):
'name': col['name'],
'type': dtype.split('(')[0] if '(' in dtype else dtype,
'longType': dtype,
'indexed': col['name'] in indexed_columns,
'keys': [
k for k in keys
if col['name'] in k.get('column_names')
],
})
tbl = {
'name': table_name,
'columns': cols,
'selectStar': mydb.select_star(
table_name, schema=schema, show_cols=True, indent=True),
'indexes': indexes,
'primaryKey': primary_key,
'foreignKeys': foreign_keys,
'indexes': keys,
}
return Response(json.dumps(tbl), mimetype="application/json")

Expand Down
22 changes: 22 additions & 0 deletions tests/core_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,28 @@ def test_templated_sql_json(self):
data = self.run_sql(sql, "admin", "fdaklj3ws")
self.assertEqual(data['data'][0]['test'], "2017-01-01T00:00:00")

def test_table_metadata(self):
maindb = self.get_main_database(db.session)
data = self.get_json_resp(
"/caravel/table/{}/ab_user/null/".format(maindb.id))
self.assertEqual(data['name'], 'ab_user')
assert len(data['columns']) > 5
assert data.get('selectStar').startswith('SELECT')

# Engine specific tests
backend = maindb.backend
if backend in ('mysql', 'postgresql'):
self.assertEqual(data.get('primaryKey').get('type'), 'pk')
self.assertEqual(
data.get('primaryKey').get('column_names')[0], 'id')
self.assertEqual(len(data.get('foreignKeys')), 2)
if backend == 'mysql':
self.assertEqual(len(data.get('indexes')), 7)
elif backend == 'postgresql':
self.assertEqual(len(data.get('indexes')), 5)



if __name__ == '__main__':
unittest.main()

0 comments on commit 61509bb

Please sign in to comment.