diff --git a/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx b/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx
new file mode 100644
index 0000000000000..24e71b207c633
--- /dev/null
+++ b/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx
@@ -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 = {col.name};
+ icons = col.keys.map((key, i) => (
+
+
+ {tooltipTitleMap[key.type]}
+
+
+ {JSON.stringify(key, null, ' ')}
+
+
+ }
+ >
+
+
+
+ ));
+ }
+ return (
+
+
+ {name}{icons}
+
+
+ {col.type}
+
+
);
+ }
+}
+ColumnElement.propTypes = propTypes;
+
+export default ColumnElement;
diff --git a/caravel/assets/javascripts/SqlLab/components/TableElement.jsx b/caravel/assets/javascripts/SqlLab/components/TableElement.jsx
index 7295a0343e6a2..52e7d2a6514f7 100644
--- a/caravel/assets/javascripts/SqlLab/components/TableElement.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/TableElement.jsx
@@ -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 = {
@@ -119,21 +120,9 @@ class TableElement extends React.PureComponent {
{this.renderHeader()}
- {cols && cols.map((col) => {
- let name = col.name;
- if (col.indexed) {
- name =
{col.name};
- }
- return (
-
-
- {name}
-
-
- {col.type}
-
-
);
- })}
+ {cols && cols.map(col => (
+
+ ))}
@@ -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 = (
@@ -157,13 +145,13 @@ class TableElement extends React.PureComponent {
Keys for table {table.name}
}
- modalBody={
- {JSON.stringify(table.indexes, null, 4)}
- }
+ modalBody={table.indexes.map((ix, i) => (
+ {JSON.stringify(ix, null, ' ')}
+ ))}
triggerNode={
}
/>
diff --git a/caravel/assets/javascripts/SqlLab/main.css b/caravel/assets/javascripts/SqlLab/main.css
index 61960f5949b5e..89701fb9806b9 100644
--- a/caravel/assets/javascripts/SqlLab/main.css
+++ b/caravel/assets/javascripts/SqlLab/main.css
@@ -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;
+}
diff --git a/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx b/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx
new file mode 100644
index 0000000000000..2d5a8a3f2d1ea
--- /dev/null
+++ b/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx
@@ -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()
+ ).to.equal(true);
+ });
+ it('renders a proper primary key', () => {
+ const wrapper = mount();
+ 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();
+ 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();
+ expect(wrapper.find('i')).to.have.length(0);
+ expect(wrapper.find('.col-name').first().text()).to.equal('last_name');
+ });
+});
diff --git a/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx b/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
index 90a73b9afdd8c..9859785547417 100644
--- a/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
+++ b/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx
@@ -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';
@@ -29,7 +30,7 @@ describe('TableElement', () => {
});
it('has 14 columns', () => {
const wrapper = shallow();
- expect(wrapper.find('div.table-column')).to.have.length(14);
+ expect(wrapper.find(ColumnElement)).to.have.length(14);
});
it('mounts', () => {
mount();
@@ -37,10 +38,10 @@ describe('TableElement', () => {
it('sorts columns', () => {
const wrapper = mount();
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();
diff --git a/caravel/assets/spec/javascripts/sqllab/fixtures.js b/caravel/assets/spec/javascripts/sqllab/fixtures.js
index f32f3db31dedf..3ff6d922f67bb 100644
--- a/caravel/assets/spec/javascripts/sqllab/fixtures.js
+++ b/caravel/assets/spec/javascripts/sqllab/fixtures.js
@@ -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,
diff --git a/caravel/models.py b/caravel/models.py
index 17fdbfb54194f..8371f83d6d5f1 100644
--- a/caravel/models.py
+++ b/caravel/models.py
@@ -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)
diff --git a/caravel/views.py b/caravel/views.py
index 5e120c44e3348..4f4d9cc93fac8 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -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 = ""
@@ -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")
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 704ada0b9a161..e65522ea1d0f1 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -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()