diff --git a/packages/cubejs-elasticsearch-driver/driver/ElasticSearchDriver.js b/packages/cubejs-elasticsearch-driver/driver/ElasticSearchDriver.js index 34da28be707fb..771b49d47f238 100644 --- a/packages/cubejs-elasticsearch-driver/driver/ElasticSearchDriver.js +++ b/packages/cubejs-elasticsearch-driver/driver/ElasticSearchDriver.js @@ -5,6 +5,9 @@ const BaseDriver = require('@cubejs-backend/query-orchestrator/driver/BaseDriver class ElasticSearchDriver extends BaseDriver { constructor(config) { super(); + + // TODO: This config applies to AWS ES, Native ES and OpenDistro ES + // All 3 have different dialects according to their respective documentation this.config = { url: process.env.CUBEJS_DB_URL, openDistro: @@ -12,7 +15,7 @@ class ElasticSearchDriver extends BaseDriver { process.env.CUBEJS_DB_TYPE === 'odelasticsearch', ...config }; - this.client = new Client({ node: this.config.url }); + this.client = new Client({ node: this.config.url, cloud: this.config.cloud }); this.sqlClient = this.config.openDistro ? new Client({ node: `${this.config.url}/_opendistro` }) : this.client; } @@ -30,6 +33,15 @@ class ElasticSearchDriver extends BaseDriver { } })).body; + // TODO: Clean this up, will need a better identifier than the cloud setting + if (this.config.cloud) { + const compiled = result.rows.map( + r => result.columns.reduce((prev, cur, idx) => ({ ...prev, [cur.name]: r[idx] }), {}) + ); + + return compiled; + } + return result && result.aggregations && this.traverseAggregations(result.aggregations); } catch (e) { if (e.body) { diff --git a/packages/cubejs-schema-compiler/adapter/ElasticSearchQuery.js b/packages/cubejs-schema-compiler/adapter/ElasticSearchQuery.js new file mode 100644 index 0000000000000..152c87cc36b96 --- /dev/null +++ b/packages/cubejs-schema-compiler/adapter/ElasticSearchQuery.js @@ -0,0 +1,108 @@ +/* eslint-disable max-classes-per-file */ +// const moment = require('moment-timezone'); +const R = require("ramda"); + +const BaseQuery = require("./BaseQuery"); +const BaseFilter = require("./BaseFilter"); + +const GRANULARITY_TO_INTERVAL = { + day: date => `DATE_TRUNC('day', ${date}::datetime)`, + week: date => `DATE_TRUNC('week', ${date}::datetime)`, + hour: date => `DATE_TRUNC('hour', ${date}::datetime)`, + minute: date => `DATE_TRUNC('minute', ${date}::datetime)`, + second: date => `DATE_TRUNC('second', ${date}::datetime)`, + month: date => `DATE_TRUNC('month', ${date}::datetime)`, + year: date => `DATE_TRUNC('year', ${date}::datetime)` +}; + +class ElasticSearchQueryFilter extends BaseFilter { + likeIgnoreCase(column, not) { + return `${not ? " NOT" : ""} MATCH(${column}, ?, 'fuzziness=AUTO:1,5')`; + } +} + +class ElasticSearchQuery extends BaseQuery { + newFilter(filter) { + return new ElasticSearchQueryFilter(this, filter); + } + + convertTz(field) { + return `${field}`; // TODO + } + + timeStampCast(value) { + return `${value}`; + } + + dateTimeCast(value) { + return `${value}`; // TODO + } + + subtractInterval(date, interval) { + // TODO: Test this, note sure how value gets populated here + return `${date} - INTERVAL ${interval}`; + } + + addInterval(date, interval) { + // TODO: Test this, note sure how value gets populated here + return `${date} + INTERVAL ${interval}`; + } + + timeGroupedColumn(granularity, dimension) { + return GRANULARITY_TO_INTERVAL[granularity](dimension); + } + + groupByClause() { + const dimensionsForSelect = this.dimensionsForSelect(); + const dimensionColumns = R.flatten( + dimensionsForSelect.map(s => s.selectColumns() && s.dimensionSql()) + ).filter(s => !!s); + + return dimensionColumns.length ? ` GROUP BY ${dimensionColumns.join(", ")}` : ""; + } + + orderHashToString(hash) { + if (!hash || !hash.id) { + return null; + } + + const fieldAlias = this.getFieldAlias(hash.id); + + if (fieldAlias === null) { + return null; + } + + const direction = hash.desc ? "DESC" : "ASC"; + return `${fieldAlias} ${direction}`; + } + + getFieldAlias(id) { + const equalIgnoreCase = (a, b) => typeof a === "string" && + typeof b === "string" && + a.toUpperCase() === b.toUpperCase(); + + let field; + + field = this.dimensionsForSelect().find(d => equalIgnoreCase(d.dimension, id)); + + if (field) { + return field.dimensionSql(); + } + + field = this.measures.find( + d => equalIgnoreCase(d.measure, id) || equalIgnoreCase(d.expressionName, id) + ); + + if (field) { + return field.aliasName(); // TODO isn't supported + } + + return null; + } + + escapeColumnName(name) { + return `${name}`; // TODO + } +} + +module.exports = ElasticSearchQuery; diff --git a/packages/cubejs-schema-compiler/adapter/QueryBuilder.js b/packages/cubejs-schema-compiler/adapter/QueryBuilder.js index 853b353f2abf6..17077ca7c25a0 100644 --- a/packages/cubejs-schema-compiler/adapter/QueryBuilder.js +++ b/packages/cubejs-schema-compiler/adapter/QueryBuilder.js @@ -12,6 +12,7 @@ const hive = require('./HiveQuery'); const oracle = require('./OracleQuery'); const sqlite = require('./SqliteQuery'); const odelasticsearch = require('./OpenDistroElasticSearchQuery'); +const elasticsearch = require('./ElasticSearchQuery'); const ADAPTERS = { postgres, @@ -30,6 +31,7 @@ const ADAPTERS = { oracle, sqlite, odelasticsearch, + elasticsearch }; exports.query = (compilers, dbType, queryOptions) => { if (!ADAPTERS[dbType]) { diff --git a/packages/cubejs-server-core/core/index.js b/packages/cubejs-server-core/core/index.js index 1a61e73a79a73..beb8e8d390a75 100644 --- a/packages/cubejs-server-core/core/index.js +++ b/packages/cubejs-server-core/core/index.js @@ -30,6 +30,7 @@ const DriverDependencies = { oracle: '@cubejs-backend/oracle-driver', sqlite: '@cubejs-backend/sqlite-driver', odelasticsearch: '@cubejs-backend/elasticsearch-driver', + elasticsearch: '@cubejs-backend/elasticsearch-driver', }; const checkEnvForPlaceholders = () => {