Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

DB Connector for Impala #203

Merged
merged 14 commits into from Oct 23, 2017
1 change: 1 addition & 0 deletions app/actions/sessions.js
Expand Up @@ -361,6 +361,7 @@ function PREVIEW_QUERY (dialect, table, database = '') {
switch (dialect) {
case DIALECTS.IBM_DB2:
return `SELECT * FROM ${table} FETCH FIRST 1000 ROWS ONLY`;
case DIALECTS.APACHE_IMPALA:
case DIALECTS.APACHE_SPARK:
case DIALECTS.MYSQL:
case DIALECTS.SQLITE:
Expand Down
Expand Up @@ -11,6 +11,7 @@ export default function DialectSelector(props) {
const logos = values(DIALECTS).map(DIALECT => (
<div
key={DIALECT}
data-tip={DIALECT}
className={classnames(
'logo', {
['logoSelected']:
Expand Down
3 changes: 3 additions & 0 deletions app/components/Settings/Settings.react.js
@@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react';
import {contains, dissoc, eqProps, flip, hasIn, head, isEmpty, keys, merge, propOr, reduce} from 'ramda';
import {connect} from 'react-redux';
import ReactToolTip from 'react-tooltip';
import classnames from 'classnames';
import * as Actions from '../../actions/sessions';
import fetch from 'isomorphic-fetch';
Expand Down Expand Up @@ -116,6 +117,7 @@ class Settings extends Component {
)}
>
<div className={'dialectSelector'}>
<ReactToolTip place={'top'} type={'dark'} effect={'solid'} />
<DialectSelector
connectionObject={connectionObject}
updateConnection={updateConnection}
Expand Down Expand Up @@ -200,6 +202,7 @@ class Settings extends Component {

const connectionObject = connections[selectedTab] || {};
if (contains(connectionObject.dialect, [
DIALECTS.APACHE_IMPALA,
DIALECTS.APACHE_SPARK,
DIALECTS.IBM_DB2,
DIALECTS.MYSQL, DIALECTS.MARIADB, DIALECTS.POSTGRES,
Expand Down
2 changes: 2 additions & 0 deletions app/components/Settings/Tabs/Tab.react.js
Expand Up @@ -17,6 +17,8 @@ export default class ConnectionTab extends Component {
label = `S3 - (${connectionObject.bucket})`;
} else if (dialect === DIALECTS.APACHE_DRILL) {
label = `Apache Drill (${connectionObject.host})`;
} else if (dialect === DIALECTS.APACHE_IMPALA) {
label = `Apache Impala (${connectionObject.host}:${connectionObject.port})`;
} else if (dialect === DIALECTS.APACHE_SPARK) {
label = `Apache Spark (${connectionObject.host}:${connectionObject.port})`;
} else if (connectionObject.dialect === DIALECTS.ELASTICSEARCH) {
Expand Down
44 changes: 31 additions & 13 deletions app/constants/constants.js
Expand Up @@ -11,6 +11,7 @@ export const DIALECTS = {
S3: 's3',
IBM_DB2: 'ibm db2',
APACHE_SPARK: 'apache spark',
APACHE_IMPALA: 'apache impala',
APACHE_DRILL: 'apache drill'
};

Expand All @@ -22,7 +23,8 @@ export const SQL_DIALECTS_USING_EDITOR = [
'mssql',
'sqlite',
'ibm db2',
'apache spark'
'apache spark',
'apache impala'
];

const commonSqlOptions = [
Expand All @@ -48,18 +50,26 @@ const commonSqlOptions = [
}
];

const hadoopQLOptions = [
{'label': 'Host', 'value': 'host', 'type': 'text' },
{'label': 'Port', 'value': 'port', 'type': 'number'},
{
'label': 'Database',
'value': 'database',
'type': 'text',
'description': 'Database Name (Optional). If database name is not specified, all tables are returned.'
},
{
'label': 'Timeout',
'value': 'timeout',
'type': 'number',
'description': 'Number of seconds for a request to timeout.'
}
]

export const CONNECTION_CONFIG = {
[DIALECTS.APACHE_SPARK]: [
{'label': 'Host', 'value': 'host', 'type': 'text' },
{'label': 'Port', 'value': 'port', 'type': 'number'},
{'label': 'Database', 'value': 'database', 'type': 'text'},
{
'label': 'Timeout',
'value': 'timeout',
'type': 'number',
'description': 'Number of seconds for a request to timeout.'
}
],
[DIALECTS.APACHE_IMPALA]: hadoopQLOptions,
[DIALECTS.APACHE_SPARK]: hadoopQLOptions,
[DIALECTS.IBM_DB2]: commonSqlOptions,
[DIALECTS.MYSQL]: commonSqlOptions,
[DIALECTS.MARIADB]: commonSqlOptions,
Expand Down Expand Up @@ -172,6 +182,7 @@ export const CONNECTION_CONFIG = {

export const LOGOS = {
[DIALECTS.APACHE_SPARK]: 'images/spark-logo.png',
[DIALECTS.APACHE_IMPALA]: 'images/impala-logo.png',
[DIALECTS.IBM_DB2]: 'images/ibmdb2-logo.png',
[DIALECTS.REDSHIFT]: 'images/redshift-logo.png',
[DIALECTS.POSTGRES]: 'images/postgres-logo.png',
Expand All @@ -188,7 +199,7 @@ export function defaultQueries(dialect, selectedTable) {

if(dialect === DIALECTS.IBM_DB2) {
return `SELECT * FROM ${selectedTable} FETCH FIRST 10 ROWS ONLY`;
} else if(dialect === DIALECTS.APACHE_SPARK) {
} else if(dialect === DIALECTS.APACHE_SPARK || dialect === DIALECTS.APACHE_IMPALA) {
return `SELECT * FROM ${selectedTable} LIMIT 10`;
} else if(dialect === DIALECTS.MSSQL) {
return `SELECT TOP 10 * \nFROM ${selectedTable};`;
Expand Down Expand Up @@ -264,6 +275,13 @@ export const FAQ = [
];

export const SAMPLE_DBS = {
[DIALECTS.APACHE_IMPALA]: {
timeout: 180,
database: 'plotly',
port: 21000,
host: '35.184.155.127',
dialect: DIALECTS.APACHE_IMPALA
},
[DIALECTS.APACHE_SPARK]: {
timeout: 180,
database: 'plotly',
Expand Down
Binary file added app/images/impala-logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions backend/persistent/datastores/Datastores.js
Expand Up @@ -4,6 +4,7 @@ import * as S3 from './S3';
import * as ApacheDrill from './ApacheDrill';
import * as IbmDb2 from './ibmdb2';
import * as ApacheLivy from './livy';
import * as ApacheImpala from './impala';

/*
* Switchboard to all of the different types of connections
Expand Down Expand Up @@ -34,6 +35,8 @@ function getDatastoreClient(connection) {
return ApacheDrill;
} else if (dialect === 'apache spark') {
return ApacheLivy;
} else if (dialect === 'apache impala') {
return ApacheImpala;
} else if (dialect === 'ibm db2') {
return IbmDb2;
}
Expand Down
93 changes: 93 additions & 0 deletions backend/persistent/datastores/impala.js
@@ -0,0 +1,93 @@
import fetch from 'node-fetch';
import * as impala from 'node-impala';
import {dissoc, keys, values, init, map, prepend, unnest} from 'ramda';

import Logger from '../../logger';


export function createClient(connection) {
const client = impala.createClient();
client.connect({
host: connection.host,
port: connection.port,
resultType: 'json-array'
});
return client;
}

export function connect(connection) {

// Runs a blank query to check connection has been established:
return createClient(connection).query('SELECT ID FROM (SELECT 1 ID) DUAL WHERE ID=0')
.catch(err => {
Logger.log(err);
throw new Error(err);
});
}

export function tables(connection) {
const code = (connection.database) ?
`show tables in ${connection.database}` :
'show tables';
return createClient(connection).query(code)
.then(json => {
let tableNames = json.map(t => t.name);
if (connection.database) tableNames = tableNames.map(tn => `${connection.database}.${tn}`);
tableNames = tableNames.map(tn => tn.toUpperCase());

return tableNames;
}).catch(err => {
Logger.log(err);
throw new Error(err);
});
}

export function schemas(connection) {
let columnnames = ['tablename', 'column_name', 'data_type'];
const showTables = (connection.database) ?
`show tables in ${connection.database}` :
'show tables';

return createClient(connection).query(showTables)
.then(json => {
let tableNames = json.map(t => t.name);
if (connection.database) tableNames = tableNames.map(tn => `${connection.database}.${tn}`);

/*
* The last column in the output of describe statement is 'comment',
* so we remove it(using Ramda.init) before sending out the result.
*/
const promises = map(tableName => {
return query(`describe ${tableName}`, connection)
.then(json => map(row => prepend(tableName, init(row)), json.rows));
}, tableNames);

// Wait for all the describe-table promises to resolve before resolving:
return Promise.all(promises);
}).then(res => {

// The results are nested inside a list, so we need to un-nest first:
const rows = unnest(res);
return {columnnames, rows};
}).catch(err => {
Logger.log(err);
throw new Error(err);
});
}

export function query(query, connection) {

return createClient(connection).query(query)
.then(json => {
let columnnames = [];
let rows = [[]];
if (json.length !== 0) {
columnnames = keys(json[0]);
rows = json.map(obj => values(obj));
}
return {columnnames, rows};
}).catch(err => {
Logger.log(err);
throw new Error(err)
});
}
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -26,6 +26,7 @@
"test-unit-certificates": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --timeout 20000 --compilers js:babel-register test/backend/certificates.spec.js",
"test-unit-ibmdb": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --timeout 20000 --compilers js:babel-register test/backend/datastores.ibmdb.spec.js",
"test-unit-livy": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --timeout 20000 --compilers js:babel-register test/backend/datastores.livy.spec.js",
"test-unit-impala": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --timeout 20000 --compilers js:babel-register test/backend/datastores.impala.spec.js",
"test-unit-routes": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --timeout 20000 --compilers js:babel-register test/backend/routes.spec.js",
"package": "cross-env NODE_ENV=production node -r babel-register package.js",
"package-all": "yarn run package -- --all",
Expand Down Expand Up @@ -194,6 +195,7 @@
"mysql": "^2.10.2",
"node-fetch": "^1.7.2",
"node-gyp": "^3.3.1",
"node-impala": "^2.0.4",
"node-libs-browser": "^1.0.0",
"node-restify": "^0.2.1",
"pg": "^4.5.5",
Expand All @@ -217,7 +219,7 @@
"react-select": "^1.0.0-beta13",
"react-split-pane": "^0.1.66",
"react-tabs": "^1.1.0",
"react-tooltip": "^3.1.7",
"react-tooltip": "^3.4.0",
"react-treeview": "^0.4.7",
"redux": "^3.4.0",
"redux-actions": "^0.9.1",
Expand Down
8 changes: 8 additions & 0 deletions sample-storage/connections.yaml
Expand Up @@ -86,3 +86,11 @@
database: plotly
port: 8998
host: 104.154.141.189

-
dialect: apache impala
id: apache impala-159e0b47-0428-4c9e-b4b9-8201b86f8ca2
timeout: 180
database: plotly
port: 21000
host: 35.184.155.127
55 changes: 55 additions & 0 deletions test/backend/datastores.impala.spec.js
@@ -0,0 +1,55 @@
import {assert} from 'chai';

import {DIALECTS} from '../../app/constants/constants.js';
import {apacheImpalaConnection as connection} from './utils.js';
import {
query, connect, tables
} from '../../backend/persistent/datastores/Datastores.js';

describe('Apache Impala:', function () {

it('connect succeeds', function() {
this.timeout(180 * 1000);
return connect(connection);
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add another test that bad credentials causes the connection to fail with an appropriate error message?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and since this is sql-like, it would be good to add this to the set of routes tests here: https://github.com/plotly/falcon-sql-client/blob/4dbbde6bfcbd43c6753053d80a283c5338bbcba4/test/backend/routes.spec.js#L544-L995

it('tables returns list of tables', function() {
return tables(connection).then(result => {
const tableName = (connection.database) ?
`${connection.database}.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010`.toUpperCase() :
'ALCOHOL_CONSUMPTION_BY_COUNTRY_2010';

assert.deepEqual(result, [tableName]);
});
});

it('query returns rows and column names', function() {
const tableName = (connection.database) ?
`${connection.database}.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010`.toUpperCase() :
'ALCOHOL_CONSUMPTION_BY_COUNTRY_2010';

return query(`SELECT * FROM ${tableName}\nLIMIT 5`, connection).then(results => {
assert.deepEqual(results.rows, [
['Belarus', "17.5"],
['Moldova', "16.8"],
['Lithuania', "15.4"],
['Russia', "15.1"],
['Romania', "14.4"]
]);
assert.deepEqual(results.columnnames, ['loc', 'alcohol']);
});
});

it('connect for invalid credentials fails', function() {
connection.host = 'http://lah-lah.lemons.com';

return connect(connection).catch(err => {
// reset hostname
connection.host = '35.184.155.127';

assert.equal(err, ('Error: Error: getaddrinfo ENOTFOUND ' +
'http://lah-lah.lemons.com ' +
'http://lah-lah.lemons.com:21000'));
});
});
});