From 2d1477f2435aae3b37ef1a27ec63daa6100d6eb1 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 16:30:44 +0530 Subject: [PATCH 01/14] Added UI updates for Impala --- app/actions/sessions.js | 1 + app/components/Settings/Settings.react.js | 1 + app/components/Settings/Tabs/Tab.react.js | 2 ++ app/constants/constants.js | 39 ++++++++++++++-------- app/images/impala-logo.png | Bin 0 -> 25804 bytes sample-storage/connections.yaml | 8 +++++ 6 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 app/images/impala-logo.png diff --git a/app/actions/sessions.js b/app/actions/sessions.js index a4211b0a6..7a7eeaf3f 100644 --- a/app/actions/sessions.js +++ b/app/actions/sessions.js @@ -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: diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js index 5a06c049f..f7bfb223c 100644 --- a/app/components/Settings/Settings.react.js +++ b/app/components/Settings/Settings.react.js @@ -200,6 +200,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, diff --git a/app/components/Settings/Tabs/Tab.react.js b/app/components/Settings/Tabs/Tab.react.js index e1e753ed4..0d8416500 100644 --- a/app/components/Settings/Tabs/Tab.react.js +++ b/app/components/Settings/Tabs/Tab.react.js @@ -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) { diff --git a/app/constants/constants.js b/app/constants/constants.js index 086c39d97..060fdd2ac 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -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' }; @@ -22,7 +23,8 @@ export const SQL_DIALECTS_USING_EDITOR = [ 'mssql', 'sqlite', 'ibm db2', - 'apache spark' + 'apache spark', + 'apache impala' ]; const commonSqlOptions = [ @@ -48,18 +50,21 @@ const commonSqlOptions = [ } ]; +const hadoopQLOptions = [ + {'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.' + } +] + 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, @@ -172,6 +177,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', @@ -188,7 +194,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};`; @@ -264,6 +270,13 @@ export const FAQ = [ ]; export const SAMPLE_DBS = { + [DIALECTS.APACHE_IMPALA]: { + timeout: 180, + database: 'plotly', + port: 21000, + host: '127.0.0.1', + dialect: DIALECTS.APACHE_IMPALA + }, [DIALECTS.APACHE_SPARK]: { timeout: 180, database: 'plotly', diff --git a/app/images/impala-logo.png b/app/images/impala-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d414518b3cd46e1ebbf96de7b6ac62b2cc32b44d GIT binary patch literal 25804 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 QG5`Po07*qoM6N<$f=IwmumAu6 literal 0 HcmV?d00001 diff --git a/sample-storage/connections.yaml b/sample-storage/connections.yaml index 9e834a471..e724e641b 100644 --- a/sample-storage/connections.yaml +++ b/sample-storage/connections.yaml @@ -86,3 +86,11 @@ database: plotly port: 8998 host: 104.154.141.189 + +- + dialect: apache spark + id: apache impala-159e0b47-0428-4c9e-b4b9-8201b86f8ca2 + timeout: 180 + database: plotly + port: 21000 + host: 127.0.0.1 From d5474f696570b506d06a533b3854f78a9893deb9 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 16:51:12 +0530 Subject: [PATCH 02/14] Added tooltip over connection-selectors Easier for users to recognize which db they are choosing. --- .../Settings/DialectSelector/DialectSelector.react.js | 1 + app/components/Settings/Settings.react.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/components/Settings/DialectSelector/DialectSelector.react.js b/app/components/Settings/DialectSelector/DialectSelector.react.js index 4fff79eb5..5f7eb9eea 100644 --- a/app/components/Settings/DialectSelector/DialectSelector.react.js +++ b/app/components/Settings/DialectSelector/DialectSelector.react.js @@ -11,6 +11,7 @@ export default function DialectSelector(props) { const logos = values(DIALECTS).map(DIALECT => (
+ Date: Wed, 18 Oct 2017 17:01:11 +0530 Subject: [PATCH 03/14] package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 907d366f8..4a4fcaf6b 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,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", From 67ae51d3739a3f147b49e1ad1720ba326a5829b6 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 19:57:21 +0530 Subject: [PATCH 04/14] Added backend for Impala. --- backend/persistent/datastores/Datastores.js | 3 + backend/persistent/datastores/impala.js | 72 +++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 backend/persistent/datastores/impala.js diff --git a/backend/persistent/datastores/Datastores.js b/backend/persistent/datastores/Datastores.js index 561252b93..88a5336a8 100644 --- a/backend/persistent/datastores/Datastores.js +++ b/backend/persistent/datastores/Datastores.js @@ -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 @@ -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; } diff --git a/backend/persistent/datastores/impala.js b/backend/persistent/datastores/impala.js new file mode 100644 index 000000000..f7052cbe4 --- /dev/null +++ b/backend/persistent/datastores/impala.js @@ -0,0 +1,72 @@ +import fetch from 'node-fetch'; +import {createClient} from 'node-impala'; +import {dissoc, keys, values, init, map, prepend, unnest} from 'ramda'; + +import Logger from '../../logger'; + +const client = createClient(); + +export function connect(connection) { + return client.connect({ + host: connection.host, + port: connection.port, + resultType: 'json-array' + }); +} + +export function tables(connection) { + const code = (connection.database) ? + `show tables in ${connection.database}` : + 'show tables'; + return client.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']; + return tables(connection) + .then(tableNames => { + + const promises = map(tableName => { + return query(`describe ${tableName}`) + .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 client.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) + }); +} From b1beb26bb6d6eee19a37b1cb9725c32159916144 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 20:10:19 +0530 Subject: [PATCH 05/14] Updated package.json and yarn.lock --- package.json | 2 +- yarn.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4a4fcaf6b..495291806 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,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", diff --git a/yarn.lock b/yarn.lock index 657508288..17fd0353f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6331,6 +6331,10 @@ nan@^2.3.0, nan@^2.3.3, nan@~2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" +nan@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-1.0.0.tgz#ae24f8850818d662fcab5acf7f3b95bfaa2ccf38" + nan@~2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/nan/-/nan-2.3.5.tgz#822a0dc266290ce4cd3a12282ca3e7e364668a08" @@ -6459,6 +6463,16 @@ node-gyp@^3.3.1, node-gyp@^3.6.0: tar "^2.0.0" which "1" +node-impala@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-impala/-/node-impala-2.0.4.tgz#57e0eb89c3a3925f8c37d8dfd7135d5ad90bf9af" + dependencies: + thrift "^0.10.0" + +node-int64@~0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.3.3.tgz#2d6e6b2ece5de8588b43d88d1bc41b26cd1fa84d" + node-libs-browser@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea" @@ -7459,7 +7473,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8: +prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -7515,6 +7529,10 @@ pure-color@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" +q@1.0.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.0.1.tgz#11872aeedee89268110b10a718448ffb10112a14" + q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" @@ -7800,12 +7818,12 @@ react-tabs@^1.1.0: classnames "^2.2.0" prop-types "^15.5.0" -react-tooltip@^3.1.7: - version "3.3.0" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-3.3.0.tgz#51c08ae0221075e2c43d83cd47fc78466612df7d" +react-tooltip@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-3.4.0.tgz#037f38f797c3e6b1b58d2534ccc8c2c76af4f52d" dependencies: - classnames "^2.2.0" - prop-types "^15.5.8" + classnames "^2.2.5" + prop-types "^15.6.0" react-transform-catch-errors@^1.0.2: version "1.0.2" @@ -9322,6 +9340,14 @@ text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +thrift@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/thrift/-/thrift-0.10.0.tgz#339af65921677b30560aa51d6f7ab1b8091c9376" + dependencies: + node-int64 "~0.3.0" + q "1.0.x" + ws "~0.4.32" + throttleit@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" @@ -9390,6 +9416,10 @@ tinycolor2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" +tinycolor@0.x: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164" + tmp@0.0.24: version "0.0.24" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.24.tgz#d6a5e198d14a9835cc6f2d7c3d9e302428c8cf12" @@ -10034,6 +10064,15 @@ ws@^1.0.1: options ">=0.0.5" ultron "1.0.x" +ws@~0.4.32: + version "0.4.32" + resolved "https://registry.yarnpkg.com/ws/-/ws-0.4.32.tgz#787a6154414f3c99ed83c5772153b20feb0cec32" + dependencies: + commander "~2.1.0" + nan "~1.0.0" + options ">=0.0.5" + tinycolor "0.x" + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" From 9e85a2bab3aed6ce3c6267bdc99037476dd0a997 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 20:18:26 +0530 Subject: [PATCH 06/14] Updated sample-db ip --- app/constants/constants.js | 2 +- sample-storage/connections.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/constants/constants.js b/app/constants/constants.js index 060fdd2ac..07c926262 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -274,7 +274,7 @@ export const SAMPLE_DBS = { timeout: 180, database: 'plotly', port: 21000, - host: '127.0.0.1', + host: '35.184.155.127', dialect: DIALECTS.APACHE_IMPALA }, [DIALECTS.APACHE_SPARK]: { diff --git a/sample-storage/connections.yaml b/sample-storage/connections.yaml index e724e641b..3bc5b282f 100644 --- a/sample-storage/connections.yaml +++ b/sample-storage/connections.yaml @@ -93,4 +93,4 @@ timeout: 180 database: plotly port: 21000 - host: 127.0.0.1 + host: 35.184.155.127 From 7248f8e6ebd145149eac05989bd66a719ed3dd7f Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 22:25:01 +0530 Subject: [PATCH 07/14] Added tests. --- package.json | 1 + test/backend/datastores.impala.spec.js | 51 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/backend/datastores.impala.spec.js diff --git a/package.json b/package.json index 495291806..807e173f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/backend/datastores.impala.spec.js b/test/backend/datastores.impala.spec.js new file mode 100644 index 000000000..a0a00c9f9 --- /dev/null +++ b/test/backend/datastores.impala.spec.js @@ -0,0 +1,51 @@ +import {assert} from 'chai'; + +import {DIALECTS} from '../../app/constants/constants.js'; + +import { + query, connect, tables +} from '../../backend/persistent/datastores/Datastores.js'; + + +const connection = { + dialect: DIALECTS.APACHE_IMPALA, + host: '35.184.155.127', // default: '127.0.0.1' + port: 21000, + database: 'plotly', // default: '' + timeout: 60 +}; + +describe('Apache Spark:', function () { + + it('connect succeeds', function() { + this.timeout(180 * 1000); + return connect(connection); + }); + + 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']); + }); + }); +}); From e8efab325aee341d90b8c56eb330a09740d9383d Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 23:00:15 +0530 Subject: [PATCH 08/14] Typos + added/updated comments --- app/constants/constants.js | 7 ++++++- backend/persistent/datastores/impala.js | 5 ++++- sample-storage/connections.yaml | 2 +- test/backend/datastores.impala.spec.js | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/constants/constants.js b/app/constants/constants.js index 07c926262..b566af946 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -53,7 +53,12 @@ const commonSqlOptions = [ const hadoopQLOptions = [ {'label': 'Host', 'value': 'host', 'type': 'text' }, {'label': 'Port', 'value': 'port', 'type': 'number'}, - {'label': 'Database', 'value': 'database', 'type': 'text'}, + { + 'label': 'Database', + 'value': 'database', + 'type': 'text', + 'description': 'Database Name (Optional). If database name is not specified, all tables are returned.' + }, { 'label': 'Timeout', 'value': 'timeout', diff --git a/backend/persistent/datastores/impala.js b/backend/persistent/datastores/impala.js index f7052cbe4..e27600da6 100644 --- a/backend/persistent/datastores/impala.js +++ b/backend/persistent/datastores/impala.js @@ -35,7 +35,10 @@ export function schemas(connection) { let columnnames = ['tablename', 'column_name', 'data_type']; return tables(connection) .then(tableNames => { - + /* + * 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}`) .then(json => map(row => prepend(tableName, init(row)), json.rows)); diff --git a/sample-storage/connections.yaml b/sample-storage/connections.yaml index 3bc5b282f..5d95aa8bb 100644 --- a/sample-storage/connections.yaml +++ b/sample-storage/connections.yaml @@ -88,7 +88,7 @@ host: 104.154.141.189 - - dialect: apache spark + dialect: apache impala id: apache impala-159e0b47-0428-4c9e-b4b9-8201b86f8ca2 timeout: 180 database: plotly diff --git a/test/backend/datastores.impala.spec.js b/test/backend/datastores.impala.spec.js index a0a00c9f9..582398d95 100644 --- a/test/backend/datastores.impala.spec.js +++ b/test/backend/datastores.impala.spec.js @@ -15,7 +15,7 @@ const connection = { timeout: 60 }; -describe('Apache Spark:', function () { +describe('Apache Impala:', function () { it('connect succeeds', function() { this.timeout(180 * 1000); From 0c7a096a1f06b5a5832ce9205d76cf31bdcc727a Mon Sep 17 00:00:00 2001 From: tarzzz Date: Wed, 18 Oct 2017 23:29:20 +0530 Subject: [PATCH 09/14] Added test for invalid credentials --- test/backend/datastores.impala.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/backend/datastores.impala.spec.js b/test/backend/datastores.impala.spec.js index 582398d95..fc5d47e54 100644 --- a/test/backend/datastores.impala.spec.js +++ b/test/backend/datastores.impala.spec.js @@ -48,4 +48,14 @@ describe('Apache Impala:', function () { 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 => { + assert.equal(err.code, 'ENOTFOUND'); + assert.equal(err.hostname, 'http://lah-lah.lemons.com'); + assert.equal(err.port, 21000); + }); + }); }); From b1c9262205af3684bd7ac0308cdfcef37b082fb4 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Sat, 21 Oct 2017 13:30:46 +0530 Subject: [PATCH 10/14] Made some updates to impala API. --- backend/persistent/datastores/impala.js | 42 +++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/backend/persistent/datastores/impala.js b/backend/persistent/datastores/impala.js index e27600da6..e5201bd58 100644 --- a/backend/persistent/datastores/impala.js +++ b/backend/persistent/datastores/impala.js @@ -1,24 +1,25 @@ import fetch from 'node-fetch'; -import {createClient} from 'node-impala'; +import * as impala from 'node-impala'; import {dissoc, keys, values, init, map, prepend, unnest} from 'ramda'; import Logger from '../../logger'; -const client = createClient(); -export function connect(connection) { - return client.connect({ +export function createClient(connection) { + const client = impala.createClient(); + client.connect({ host: connection.host, port: connection.port, resultType: 'json-array' }); + return client; } export function tables(connection) { const code = (connection.database) ? `show tables in ${connection.database}` : 'show tables'; - return client.query(code) + return createClient(connection).query(code) .then(json => { let tableNames = json.map(t => t.name); if (connection.database) tableNames = tableNames.map(tn => `${connection.database}.${tn}`); @@ -33,14 +34,21 @@ export function tables(connection) { export function schemas(connection) { let columnnames = ['tablename', 'column_name', 'data_type']; - return tables(connection) - .then(tableNames => { + 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}`) + return query(`describe ${tableName}`, connection) .then(json => map(row => prepend(tableName, init(row)), json.rows)); }, tableNames); @@ -58,16 +66,16 @@ export function schemas(connection) { } export function query(query, connection) { - return client.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}; + 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) From b026a1ff104737c873205e94e8bbe45c5d98b780 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Sat, 21 Oct 2017 13:31:08 +0530 Subject: [PATCH 11/14] Added routes tests for impala --- test/backend/routes.spec.js | 23 +++++++++++++++++++++-- test/backend/utils.js | 12 ++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/test/backend/routes.spec.js b/test/backend/routes.spec.js index e6f9a5770..93ab70866 100644 --- a/test/backend/routes.spec.js +++ b/test/backend/routes.spec.js @@ -559,6 +559,9 @@ describe('Routes - ', () => { `${connection.database}.dbo.ebola_2014` ); } + if (connection.dialect === 'apache impala') { + sampleQuery = 'SELECT * FROM PLOTLY.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 LIMIT 1'; + } POST(`connections/${connectionId}/query`, { query: sampleQuery }) @@ -569,6 +572,8 @@ describe('Routes - ', () => { expectedColumnNames = ['Country', 'Month', 'Year', 'Lat', 'Lon', 'Value']; } else if (connection.dialect === 'sqlite') { expectedColumnNames = ['index', 'Country', 'Month', 'Year', 'Lat', 'Lon', 'Value']; + } else if (connection.dialect === 'apache impala') { + expectedColumnNames = ['loc', 'alcohol']; } else { expectedColumnNames = ['country', 'month', 'year', 'lat', 'lon', 'value']; } @@ -583,7 +588,8 @@ describe('Routes - ', () => { 'mysql': ['Guinea', 3, 14, 10, -10, '122'], 'mariadb': ['Guinea', 3, 14, 10, -10, '122'], 'mssql': ['Guinea', 3, 14, 10, -10, '122'], - 'sqlite': [0, 'Guinea', 3, 14, 9.95, -9.7, 122] + 'sqlite': [0, 'Guinea', 3, 14, 9.95, -9.7, 122], + 'apache impala': ['Belarus', '17.5'] })[connection.dialect] ], columnnames: expectedColumnNames @@ -615,7 +621,8 @@ describe('Routes - ', () => { 'corresponds to your MariaDB server version for the right syntax to use near ' + '\'SELECZ\' at line 1', mssql: "Could not find stored procedure 'SELECZ'.", - sqlite: 'SQLITE_ERROR: near "SELECZ": syntax error' + sqlite: 'SQLITE_ERROR: near "SELECZ": syntax error', + 'apache impala': 'BeeswaxException: AnalysisException: Syntax error in line 1:\nSELECZ\n^\nEncountered: IDENTIFIER\nExpected: ALTER, COMPUTE, CREATE, DESCRIBE, DROP, EXPLAIN, INSERT, INVALIDATE, LOAD, REFRESH, SELECT, SHOW, USE, VALUES, WITH\n\nCAUSED BY: Exception: Syntax error' })[connection.dialect] }} ); @@ -634,6 +641,10 @@ describe('Routes - ', () => { `${connection.database}.dbo.ebola_2014` ); } + if (connection.dialect === 'apache impala') { + query = 'SELECT * FROM PLOTLY.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 LIMIT 0'; + } + POST(`connections/${connectionId}/query`, {query}) .then(res => res.json().then(json => { assert.equal(res.status, 200); @@ -679,6 +690,9 @@ describe('Routes - ', () => { 'spatial_ref_sys' ]).sort(); } + if (connection.dialect === 'apache impala') { + tables = ['PLOTLY.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010']; + } assert.deepEqual( json, tables ); @@ -906,6 +920,11 @@ describe('Routes - ', () => { [ 'apple_stock_2014', 'AAPL_x', 'datetime', 8, '23/3' ], [ 'apple_stock_2014', 'AAPL_y', 'decimal', 17, '38/38' ] ]; + } else if (connection.dialect === 'apache impala') { + rows = [ + [ 'plotly.alcohol_consumption_by_country_2010', 'loc', 'string' ], + [ 'plotly.alcohol_consumption_by_country_2010', 'alcohol', 'double' ], + ]; } else { rows = [ [ 'alcohol_consumption_by_country_2010', 'location', 'varchar' ], diff --git a/test/backend/utils.js b/test/backend/utils.js index 21b501953..e01c3e149 100644 --- a/test/backend/utils.js +++ b/test/backend/utils.js @@ -131,6 +131,12 @@ export const sqliteConnection = { dialect: 'sqlite', storage: `${__dirname}/plotly_datasets.db` }; +export const apacheImpalaConnection = { + dialect: 'apache impala', + host: '35.184.155.127', + port: 21000, + database: 'plotly' +}; // TODO - Add sqlite here // TODO - Add postgis in here @@ -144,7 +150,8 @@ export const testConnections = [ sqliteConnection, elasticsearchConnections, publicReadableS3Connections, - apacheDrillConnections + apacheDrillConnections, + apacheImpalaConnection ]; export const testSqlConnections = [ @@ -152,7 +159,8 @@ export const testSqlConnections = [ mysqlConnection, mariadbConnection, redshiftConnection, - mssqlConnection + mssqlConnection, + apacheImpalaConnection ]; export const configuration = dissoc('password', sqlConnections); From 479304dbec3678c14e8a7f17ff0bc4df869ed721 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Sat, 21 Oct 2017 13:55:50 +0530 Subject: [PATCH 12/14] Added back 'connect' method --- backend/persistent/datastores/impala.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/persistent/datastores/impala.js b/backend/persistent/datastores/impala.js index e5201bd58..9a271d658 100644 --- a/backend/persistent/datastores/impala.js +++ b/backend/persistent/datastores/impala.js @@ -15,6 +15,12 @@ export function createClient(connection) { 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'); +} + export function tables(connection) { const code = (connection.database) ? `show tables in ${connection.database}` : From ae2fff96b2e35372f2fcebbc6954bb1f32761326 Mon Sep 17 00:00:00 2001 From: tarzzz Date: Sat, 21 Oct 2017 15:20:21 +0530 Subject: [PATCH 13/14] Fixed tests --- backend/persistent/datastores/impala.js | 6 +++++- test/backend/datastores.impala.spec.js | 6 +++--- test/backend/routes.spec.js | 7 +++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/persistent/datastores/impala.js b/backend/persistent/datastores/impala.js index 9a271d658..3f10a6415 100644 --- a/backend/persistent/datastores/impala.js +++ b/backend/persistent/datastores/impala.js @@ -18,7 +18,11 @@ export function createClient(connection) { 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'); + 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) { diff --git a/test/backend/datastores.impala.spec.js b/test/backend/datastores.impala.spec.js index fc5d47e54..d1052dc2c 100644 --- a/test/backend/datastores.impala.spec.js +++ b/test/backend/datastores.impala.spec.js @@ -53,9 +53,9 @@ describe('Apache Impala:', function () { connection.host = 'http://lah-lah.lemons.com'; return connect(connection).catch(err => { - assert.equal(err.code, 'ENOTFOUND'); - assert.equal(err.hostname, 'http://lah-lah.lemons.com'); - assert.equal(err.port, 21000); + assert.equal(err, ('Error: Error: getaddrinfo ENOTFOUND ' + + 'http://lah-lah.lemons.com ' + + 'http://lah-lah.lemons.com:21000')); }); }); }); diff --git a/test/backend/routes.spec.js b/test/backend/routes.spec.js index 93ab70866..6fa5bb0c7 100644 --- a/test/backend/routes.spec.js +++ b/test/backend/routes.spec.js @@ -1219,9 +1219,7 @@ describe('Routes - ', () => { connectionTypo = merge(connection, {username: 'typo'}); } else if (connection.dialect === 's3') { connectionTypo = merge(connection, {secretAccessKey: 'typo'}); - } else if (connection.dialect === 'elasticsearch') { - connectionTypo = merge(connection, {host: 'https://lahlahlemons.com'}); - } else if (connection.dialect === 'apache drill') { + } else if (contains(connection.dialect, ['elasticsearch', 'apache drill', 'apache impala'])) { connectionTypo = merge(connection, {host: 'https://lahlahlemons.com'}); } else if (connection.dialect === 'sqlite') { connectionTypo = merge(connection, {storage: 'typo'}); @@ -1249,7 +1247,8 @@ describe('Routes - ', () => { 'failed, reason: getaddrinfo ENOTFOUND lahlahlemons.com lahlahlemons.com:9243', ['apache drill']: 'request to https://lahlahlemons.com:8047/query.json failed, ' + 'reason: getaddrinfo ENOTFOUND lahlahlemons.com lahlahlemons.com:8047', - sqlite: 'SQLite file at path "typo" does not exist.' + sqlite: 'SQLite file at path "typo" does not exist.', + ['apache impala']: 'Error: getaddrinfo ENOTFOUND https://lahlahlemons.com https://lahlahlemons.com:21000' })[connection.dialect] } }); From 1a42a7f3378410a8c28d1f633635bbaf39ea240f Mon Sep 17 00:00:00 2001 From: tarzzz Date: Tue, 24 Oct 2017 00:37:17 +0530 Subject: [PATCH 14/14] Use connection details from utils.js --- test/backend/datastores.impala.spec.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/backend/datastores.impala.spec.js b/test/backend/datastores.impala.spec.js index d1052dc2c..0d81ce3b4 100644 --- a/test/backend/datastores.impala.spec.js +++ b/test/backend/datastores.impala.spec.js @@ -1,20 +1,11 @@ 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'; - -const connection = { - dialect: DIALECTS.APACHE_IMPALA, - host: '35.184.155.127', // default: '127.0.0.1' - port: 21000, - database: 'plotly', // default: '' - timeout: 60 -}; - describe('Apache Impala:', function () { it('connect succeeds', function() { @@ -53,6 +44,9 @@ describe('Apache Impala:', 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'));