Skip to content

Commit

Permalink
feat: ungrouped queries support
Browse files Browse the repository at this point in the history
  • Loading branch information
paveltiunov committed Oct 11, 2019
1 parent 9293f13 commit c6ac873
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 4 deletions.
5 changes: 5 additions & 0 deletions docs/Cube.js-Backend/@cubejs-backend-server-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Both [CubejsServerCore](@cubejs-backend-server-core) and [CubejsServer](@cubejs-
preAggregationsSchema: String | (context: RequestContext) => String,
schemaVersion: (context: RequestContext) => String,
telemetry: Boolean,
allowUngroupedWithoutPrimaryKey: Boolean,
orchestratorOptions: {
redisPrefix: String,
queryCacheOptions: {
Expand Down Expand Up @@ -252,6 +253,10 @@ CubejsServerCore.create({
});
```

### allowUngroupedWithoutPrimaryKey

Providing `allowUngroupedWithoutPrimaryKey: true` disables primary key inclusion check for `ungrouped` queries.

### telemetry

Cube.js collects high-level anonymous usage statistics for servers started in development mode. It doesn't track any credentials, schema contents or queries issued. This statistics is used solely for the purpose of constant cube.js improvement.
Expand Down
3 changes: 3 additions & 0 deletions docs/Cube.js-Frontend/Query-Format.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Query has the following properties:
fields to order is based on the order of the keys in the object.
- `timezone`: All time based calculations performed within Cube.js are timezone-aware. Using this property you can set your desired timezone in [TZ Database Name](https://en.wikipedia.org/wiki/Tz_database) format, e.g.: `America/Los_Angeles`. The default value is `UTC`.
- `renewQuery`: If `renewQuery` is set to `true`, query will always refresh cache and return the latest data from the database. The default value is `false`.
- `ungrouped`: If `ungrouped` is set to `true` no `GROUP BY` statement will be added to the query and raw results after filtering and joining will be returned.
By default `ungrouped` query requires to pass primary key as a dimension of every cube involved in query for security purpose.
To disable this behavior please see [allowUngroupedWithoutPrimaryKey](@cubejs-backend-server-core#allow-ungrouped-without-primary-key) server option.

```js
{
Expand Down
3 changes: 2 additions & 1 deletion packages/cubejs-api-gateway/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ const querySchema = Joi.object().keys({
timezone: Joi.string(),
limit: Joi.number().integer().min(1).max(50000),
offset: Joi.number().integer().min(0),
renewQuery: Joi.boolean()
renewQuery: Joi.boolean(),
ungrouped: Joi.boolean()
});

const normalizeQuery = (query) => {
Expand Down
26 changes: 25 additions & 1 deletion packages/cubejs-schema-compiler/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ class BaseQuery {
}

this.externalQueryClass = this.options.externalQueryClass;
this.initUngrouped();
}

initUngrouped() {
this.ungrouped = this.options.ungrouped;
if (this.ungrouped) {
if (!this.options.allowUngroupedWithoutPrimaryKey) {
const cubes = R.uniq([this.join.root].concat(this.join.joins.map(j => j.originalTo)));
const primaryKeyNames = cubes.map(c => this.primaryKeyName(c));
const missingPrimaryKeys = primaryKeyNames.filter(key => !this.dimensions.find(d => d.dimension === key));
if (missingPrimaryKeys.length) {
throw new UserError(`Ungrouped query requires primary keys to be present in dimensions: ${missingPrimaryKeys.map(k => `'${k}'`).join(', ')}. Pass allowUngroupedWithoutPrimaryKey option to disable this check.`);
}
}
if (this.measures.length) {
throw new UserError(`Measures aren't allowed in ungrouped query`);
}
if (this.measureFilters.length) {
throw new UserError(`Measure filters aren't allowed in ungrouped query`);
}
}
}

get subQueryDimensions() {
Expand Down Expand Up @@ -162,7 +183,7 @@ class BaseQuery {
}

buildParamAnnotatedSql() {
if (!this.options.preAggregationQuery) {
if (!this.options.preAggregationQuery && !this.ungrouped) {
const preAggregationForQuery = this.preAggregations.findPreAggregationForQuery();
if (preAggregationForQuery) {
return this.preAggregations.rollupPreAggregation(preAggregationForQuery);
Expand Down Expand Up @@ -740,6 +761,9 @@ class BaseQuery {
}

groupByClause() {
if (this.ungrouped) {
return '';
}
const dimensionColumns = this.dimensionColumns();
return dimensionColumns.length ? ` GROUP BY ${dimensionColumns.map((c, i) => `${i + 1}`).join(', ')}` : '';
}
Expand Down
64 changes: 64 additions & 0 deletions packages/cubejs-schema-compiler/test/SQLGenerationTest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* globals it, describe, after */
/* eslint-disable quote-props */
const UserError = require('../compiler/UserError');
const PostgresQuery = require('../adapter/PostgresQuery');
Expand Down Expand Up @@ -1336,4 +1337,67 @@ describe('SQL Generation', function test() {
}
])
);

it('ungrouped', () => runQueryTest({
measures: [],
dimensions: [
'visitors.id'
],
timeDimensions: [{
dimension: 'visitors.created_at',
granularity: 'date',
dateRange: ['2016-01-09', '2017-01-10']
}],
order: [{
id: 'visitors.created_at'
}],
timezone: 'America/Los_Angeles',
ungrouped: true
}, [{
"visitors__id": 6,
"visitors__created_at_date": "2016-09-06T00:00:00.000Z"
}, {
"visitors__id": 1,
"visitors__created_at_date": "2017-01-02T00:00:00.000Z"
}, {
"visitors__id": 2,
"visitors__created_at_date": "2017-01-04T00:00:00.000Z"
}, {
"visitors__id": 3,
"visitors__created_at_date": "2017-01-05T00:00:00.000Z"
}, {
"visitors__id": 4,
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
}, {
"visitors__id": 5,
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
}]));

it('ungrouped without id', () => runQueryTest({
measures: [],
dimensions: [],
timeDimensions: [{
dimension: 'visitors.created_at',
granularity: 'date',
dateRange: ['2016-01-09', '2017-01-10']
}],
order: [{
id: 'visitors.created_at'
}],
timezone: 'America/Los_Angeles',
ungrouped: true,
allowUngroupedWithoutPrimaryKey: true
}, [{
"visitors__created_at_date": "2016-09-06T00:00:00.000Z"
}, {
"visitors__created_at_date": "2017-01-02T00:00:00.000Z"
}, {
"visitors__created_at_date": "2017-01-04T00:00:00.000Z"
}, {
"visitors__created_at_date": "2017-01-05T00:00:00.000Z"
}, {
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
}, {
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
}]));
});
4 changes: 3 additions & 1 deletion packages/cubejs-server-core/core/CompilerApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class CompilerApi {
this.allowNodeRequire = options.allowNodeRequire == null ? true : options.allowNodeRequire;
this.logger = this.options.logger;
this.preAggregationsSchema = this.options.preAggregationsSchema;
this.allowUngroupedWithoutPrimaryKey = this.options.allowUngroupedWithoutPrimaryKey;
}

async getCompilers() {
Expand Down Expand Up @@ -39,7 +40,8 @@ class CompilerApi {
this.dbType, {
...query,
externalDbType: this.options.externalDbType,
preAggregationsSchema: this.preAggregationsSchema
preAggregationsSchema: this.preAggregationsSchema,
allowUngroupedWithoutPrimaryKey: this.allowUngroupedWithoutPrimaryKey
}
);
return (await this.getCompilers()).compiler.withQuery(sqlGenerator, () => ({
Expand Down
3 changes: 2 additions & 1 deletion packages/cubejs-server-core/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ class CubejsServerCore {
devServer: this.options.devServer,
logger: this.logger,
externalDbType: options.externalDbType,
preAggregationsSchema: options.preAggregationsSchema
preAggregationsSchema: options.preAggregationsSchema,
allowUngroupedWithoutPrimaryKey: this.options.allowUngroupedWithoutPrimaryKey
});
}

Expand Down

0 comments on commit c6ac873

Please sign in to comment.