Skip to content

Commit c6ac873

Browse files
committed
feat: ungrouped queries support
1 parent 9293f13 commit c6ac873

File tree

7 files changed

+104
-4
lines changed

7 files changed

+104
-4
lines changed

docs/Cube.js-Backend/@cubejs-backend-server-core.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Both [CubejsServerCore](@cubejs-backend-server-core) and [CubejsServer](@cubejs-
5555
preAggregationsSchema: String | (context: RequestContext) => String,
5656
schemaVersion: (context: RequestContext) => String,
5757
telemetry: Boolean,
58+
allowUngroupedWithoutPrimaryKey: Boolean,
5859
orchestratorOptions: {
5960
redisPrefix: String,
6061
queryCacheOptions: {
@@ -252,6 +253,10 @@ CubejsServerCore.create({
252253
});
253254
```
254255

256+
### allowUngroupedWithoutPrimaryKey
257+
258+
Providing `allowUngroupedWithoutPrimaryKey: true` disables primary key inclusion check for `ungrouped` queries.
259+
255260
### telemetry
256261

257262
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.

docs/Cube.js-Frontend/Query-Format.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Query has the following properties:
3030
fields to order is based on the order of the keys in the object.
3131
- `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`.
3232
- `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`.
33+
- `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.
34+
By default `ungrouped` query requires to pass primary key as a dimension of every cube involved in query for security purpose.
35+
To disable this behavior please see [allowUngroupedWithoutPrimaryKey](@cubejs-backend-server-core#allow-ungrouped-without-primary-key) server option.
3336

3437
```js
3538
{

packages/cubejs-api-gateway/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ const querySchema = Joi.object().keys({
130130
timezone: Joi.string(),
131131
limit: Joi.number().integer().min(1).max(50000),
132132
offset: Joi.number().integer().min(0),
133-
renewQuery: Joi.boolean()
133+
renewQuery: Joi.boolean(),
134+
ungrouped: Joi.boolean()
134135
});
135136

136137
const normalizeQuery = (query) => {

packages/cubejs-schema-compiler/adapter/BaseQuery.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@ class BaseQuery {
7878
}
7979

8080
this.externalQueryClass = this.options.externalQueryClass;
81+
this.initUngrouped();
82+
}
83+
84+
initUngrouped() {
85+
this.ungrouped = this.options.ungrouped;
86+
if (this.ungrouped) {
87+
if (!this.options.allowUngroupedWithoutPrimaryKey) {
88+
const cubes = R.uniq([this.join.root].concat(this.join.joins.map(j => j.originalTo)));
89+
const primaryKeyNames = cubes.map(c => this.primaryKeyName(c));
90+
const missingPrimaryKeys = primaryKeyNames.filter(key => !this.dimensions.find(d => d.dimension === key));
91+
if (missingPrimaryKeys.length) {
92+
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.`);
93+
}
94+
}
95+
if (this.measures.length) {
96+
throw new UserError(`Measures aren't allowed in ungrouped query`);
97+
}
98+
if (this.measureFilters.length) {
99+
throw new UserError(`Measure filters aren't allowed in ungrouped query`);
100+
}
101+
}
81102
}
82103

83104
get subQueryDimensions() {
@@ -162,7 +183,7 @@ class BaseQuery {
162183
}
163184

164185
buildParamAnnotatedSql() {
165-
if (!this.options.preAggregationQuery) {
186+
if (!this.options.preAggregationQuery && !this.ungrouped) {
166187
const preAggregationForQuery = this.preAggregations.findPreAggregationForQuery();
167188
if (preAggregationForQuery) {
168189
return this.preAggregations.rollupPreAggregation(preAggregationForQuery);
@@ -740,6 +761,9 @@ class BaseQuery {
740761
}
741762

742763
groupByClause() {
764+
if (this.ungrouped) {
765+
return '';
766+
}
743767
const dimensionColumns = this.dimensionColumns();
744768
return dimensionColumns.length ? ` GROUP BY ${dimensionColumns.map((c, i) => `${i + 1}`).join(', ')}` : '';
745769
}

packages/cubejs-schema-compiler/test/SQLGenerationTest.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* globals it, describe, after */
12
/* eslint-disable quote-props */
23
const UserError = require('../compiler/UserError');
34
const PostgresQuery = require('../adapter/PostgresQuery');
@@ -1336,4 +1337,67 @@ describe('SQL Generation', function test() {
13361337
}
13371338
])
13381339
);
1340+
1341+
it('ungrouped', () => runQueryTest({
1342+
measures: [],
1343+
dimensions: [
1344+
'visitors.id'
1345+
],
1346+
timeDimensions: [{
1347+
dimension: 'visitors.created_at',
1348+
granularity: 'date',
1349+
dateRange: ['2016-01-09', '2017-01-10']
1350+
}],
1351+
order: [{
1352+
id: 'visitors.created_at'
1353+
}],
1354+
timezone: 'America/Los_Angeles',
1355+
ungrouped: true
1356+
}, [{
1357+
"visitors__id": 6,
1358+
"visitors__created_at_date": "2016-09-06T00:00:00.000Z"
1359+
}, {
1360+
"visitors__id": 1,
1361+
"visitors__created_at_date": "2017-01-02T00:00:00.000Z"
1362+
}, {
1363+
"visitors__id": 2,
1364+
"visitors__created_at_date": "2017-01-04T00:00:00.000Z"
1365+
}, {
1366+
"visitors__id": 3,
1367+
"visitors__created_at_date": "2017-01-05T00:00:00.000Z"
1368+
}, {
1369+
"visitors__id": 4,
1370+
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
1371+
}, {
1372+
"visitors__id": 5,
1373+
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
1374+
}]));
1375+
1376+
it('ungrouped without id', () => runQueryTest({
1377+
measures: [],
1378+
dimensions: [],
1379+
timeDimensions: [{
1380+
dimension: 'visitors.created_at',
1381+
granularity: 'date',
1382+
dateRange: ['2016-01-09', '2017-01-10']
1383+
}],
1384+
order: [{
1385+
id: 'visitors.created_at'
1386+
}],
1387+
timezone: 'America/Los_Angeles',
1388+
ungrouped: true,
1389+
allowUngroupedWithoutPrimaryKey: true
1390+
}, [{
1391+
"visitors__created_at_date": "2016-09-06T00:00:00.000Z"
1392+
}, {
1393+
"visitors__created_at_date": "2017-01-02T00:00:00.000Z"
1394+
}, {
1395+
"visitors__created_at_date": "2017-01-04T00:00:00.000Z"
1396+
}, {
1397+
"visitors__created_at_date": "2017-01-05T00:00:00.000Z"
1398+
}, {
1399+
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
1400+
}, {
1401+
"visitors__created_at_date": "2017-01-06T00:00:00.000Z"
1402+
}]));
13391403
});

packages/cubejs-server-core/core/CompilerApi.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class CompilerApi {
1010
this.allowNodeRequire = options.allowNodeRequire == null ? true : options.allowNodeRequire;
1111
this.logger = this.options.logger;
1212
this.preAggregationsSchema = this.options.preAggregationsSchema;
13+
this.allowUngroupedWithoutPrimaryKey = this.options.allowUngroupedWithoutPrimaryKey;
1314
}
1415

1516
async getCompilers() {
@@ -39,7 +40,8 @@ class CompilerApi {
3940
this.dbType, {
4041
...query,
4142
externalDbType: this.options.externalDbType,
42-
preAggregationsSchema: this.preAggregationsSchema
43+
preAggregationsSchema: this.preAggregationsSchema,
44+
allowUngroupedWithoutPrimaryKey: this.allowUngroupedWithoutPrimaryKey
4345
}
4446
);
4547
return (await this.getCompilers()).compiler.withQuery(sqlGenerator, () => ({

packages/cubejs-server-core/core/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ class CubejsServerCore {
262262
devServer: this.options.devServer,
263263
logger: this.logger,
264264
externalDbType: options.externalDbType,
265-
preAggregationsSchema: options.preAggregationsSchema
265+
preAggregationsSchema: options.preAggregationsSchema,
266+
allowUngroupedWithoutPrimaryKey: this.options.allowUngroupedWithoutPrimaryKey
266267
});
267268
}
268269

0 commit comments

Comments
 (0)