Skip to content

Commit 172c39b

Browse files
authored
fix: Limit override isn't respected in queryRewrite (#6605)
1 parent 501b6d1 commit 172c39b

File tree

4 files changed

+185
-55
lines changed

4 files changed

+185
-55
lines changed

packages/cubejs-api-gateway/src/gateway.ts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ import {
7171
normalizeQuery,
7272
normalizeQueryCancelPreAggregations,
7373
normalizeQueryPreAggregationPreview,
74-
normalizeQueryPreAggregations,
75-
validatePostRewrite,
74+
normalizeQueryPreAggregations, remapToQueryAdapterFormat,
7675
} from './query';
7776
import { cachedHandler } from './cached-handler';
7877
import { createJWKsFetcher } from './jwk';
@@ -1101,20 +1100,39 @@ class ApiGateway {
11011100

11021101
const queries = Array.isArray(query) ? query : [query];
11031102

1104-
const normalizedQueries: NormalizedQuery[] = await Promise.all(
1103+
this.log({
1104+
type: 'Query Rewrite',
1105+
query
1106+
}, context);
1107+
1108+
let duration = 0;
1109+
1110+
let normalizedQueries: NormalizedQuery[] = await Promise.all(
11051111
queries.map(
1106-
async (currentQuery) => validatePostRewrite(
1107-
await this.queryRewrite(
1108-
normalizeQuery(currentQuery),
1109-
context
1110-
)
1111-
)
1112+
async (currentQuery) => {
1113+
const normalizedQuery = normalizeQuery(currentQuery, persistent);
1114+
const startTime = new Date().getTime();
1115+
const rewrite = await this.queryRewrite(
1116+
normalizedQuery,
1117+
context,
1118+
);
1119+
duration += new Date().getTime() - startTime;
1120+
return normalizeQuery(
1121+
rewrite,
1122+
persistent,
1123+
);
1124+
}
11121125
)
11131126
);
11141127

1115-
normalizedQueries.forEach((q) => {
1116-
this.processQueryLimit(q, persistent);
1117-
});
1128+
this.log({
1129+
type: 'Query Rewrite completed',
1130+
normalizedQueries,
1131+
duration,
1132+
query
1133+
}, context);
1134+
1135+
normalizedQueries = normalizedQueries.map(q => remapToQueryAdapterFormat(q));
11181136

11191137
if (normalizedQueries.find((currentQuery) => !currentQuery)) {
11201138
throw new Error('queryTransformer returned null query. Please check your queryTransformer implementation');
@@ -1134,29 +1152,6 @@ class ApiGateway {
11341152
return [queryType, normalizedQueries];
11351153
}
11361154

1137-
/**
1138-
* Asserts query limit, sets the default value if neccessary.
1139-
*
1140-
* @throw {Error}
1141-
*/
1142-
public processQueryLimit(query: NormalizedQuery, persistent = false): void {
1143-
const def = getEnv('dbQueryDefaultLimit') <= getEnv('dbQueryLimit')
1144-
? getEnv('dbQueryDefaultLimit')
1145-
: getEnv('dbQueryLimit');
1146-
1147-
if (!persistent) {
1148-
if (
1149-
typeof query.rowLimit === 'number' &&
1150-
query.rowLimit > getEnv('dbQueryLimit')
1151-
) {
1152-
throw new Error('The query limit has been exceeded.');
1153-
}
1154-
query.rowLimit = typeof query.rowLimit === 'number'
1155-
? query.rowLimit
1156-
: def;
1157-
}
1158-
}
1159-
11601155
public async sql({ query, context, res }: QueryRequest) {
11611156
const requestStarted = new Date();
11621157

packages/cubejs-api-gateway/src/query.js

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import R from 'ramda';
22
import moment from 'moment';
33
import Joi from 'joi';
4+
import { getEnv } from '@cubejs-backend/shared';
45

56
import { UserError } from './UserError';
67
import { dateParser } from './dateParser';
@@ -106,10 +107,7 @@ const querySchema = Joi.object().keys({
106107

107108
const normalizeQueryOrder = order => {
108109
let result = [];
109-
const normalizeOrderItem = (k, direction) => ({
110-
id: k,
111-
desc: direction === 'desc'
112-
});
110+
const normalizeOrderItem = (k, direction) => ([k, direction]);
113111
if (order) {
114112
result = Array.isArray(order) ?
115113
order.map(([k, direction]) => normalizeOrderItem(k, direction)) :
@@ -148,25 +146,13 @@ const checkQueryFilters = (filter) => {
148146
return true;
149147
};
150148

151-
const validatePostRewrite = (query) => {
152-
const validQuery = query.measures && query.measures.length ||
153-
query.dimensions && query.dimensions.length ||
154-
query.timeDimensions && query.timeDimensions.filter(td => !!td.granularity).length;
155-
if (!validQuery) {
156-
throw new UserError(
157-
'Query should contain either measures, dimensions or timeDimensions with granularities in order to be valid'
158-
);
159-
}
160-
return query;
161-
};
162-
163149
/**
164150
* Normalize incoming network query.
165151
* @param {Query} query
166152
* @throws {UserError}
167153
* @returns {NormalizedQuery}
168154
*/
169-
const normalizeQuery = (query) => {
155+
const normalizeQuery = (query, persistent) => {
170156
const { error } = querySchema.validate(query);
171157
if (error) {
172158
throw new UserError(`Invalid query format: ${error.message || error.toString()}`);
@@ -187,9 +173,29 @@ const normalizeQuery = (query) => {
187173
granularity: d.split('.')[2]
188174
}));
189175
const timezone = query.timezone || 'UTC';
176+
177+
const def = getEnv('dbQueryDefaultLimit') <= getEnv('dbQueryLimit')
178+
? getEnv('dbQueryDefaultLimit')
179+
: getEnv('dbQueryLimit');
180+
181+
let newLimit;
182+
if (!persistent) {
183+
if (
184+
typeof query.limit === 'number' &&
185+
query.limit > getEnv('dbQueryLimit')
186+
) {
187+
throw new Error('The query limit has been exceeded.');
188+
}
189+
newLimit = typeof query.limit === 'number'
190+
? query.limit
191+
: def;
192+
} else {
193+
newLimit = query.limit;
194+
}
195+
190196
return {
191197
...query,
192-
rowLimit: query.rowLimit || query.limit,
198+
limit: newLimit,
193199
timezone,
194200
order: normalizeQueryOrder(query.order),
195201
filters: (query.filters || []).map(f => {
@@ -233,6 +239,26 @@ const normalizeQuery = (query) => {
233239
};
234240
};
235241

242+
const remapQueryOrder = order => {
243+
let result = [];
244+
const normalizeOrderItem = (k, direction) => ({
245+
id: k,
246+
desc: direction === 'desc'
247+
});
248+
if (order) {
249+
result = Array.isArray(order) ?
250+
order.map(([k, direction]) => normalizeOrderItem(k, direction)) :
251+
Object.keys(order).map(k => normalizeOrderItem(k, order[k]));
252+
}
253+
return result;
254+
};
255+
256+
const remapToQueryAdapterFormat = (query) => (query ? {
257+
...query,
258+
rowLimit: query.limit,
259+
order: remapQueryOrder(query.order),
260+
} : query);
261+
236262
const queryPreAggregationsSchema = Joi.object().keys({
237263
metadata: Joi.object(),
238264
timezone: Joi.string(),
@@ -297,9 +323,9 @@ const normalizeQueryCancelPreAggregations = query => {
297323
export {
298324
getQueryGranularity,
299325
getPivotQuery,
300-
validatePostRewrite,
301326
normalizeQuery,
302327
normalizeQueryPreAggregations,
303328
normalizeQueryPreAggregationPreview,
304329
normalizeQueryCancelPreAggregations,
330+
remapToQueryAdapterFormat,
305331
};

packages/cubejs-api-gateway/src/types/query.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ interface Query {
6161
offset?: number;
6262
total?: boolean;
6363
totalQuery?: boolean;
64-
order?: QueryOrderType;
64+
order?: any;
6565
timezone?: string;
6666
renewQuery?: boolean;
6767
ungrouped?: boolean;
@@ -81,6 +81,7 @@ interface NormalizedQueryFilter extends QueryFilter {
8181
interface NormalizedQuery extends Query {
8282
filters?: NormalizedQueryFilter[];
8383
rowLimit?: null | number;
84+
order?: [{ id: string; desc: boolean }];
8485
}
8586

8687
export {

packages/cubejs-api-gateway/test/index.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ describe('API Gateway', () => {
260260
order: [],
261261
filters: [],
262262
rowLimit: 10000,
263+
limit: 10000,
263264
dimensions: [],
264265
timeDimensions: [],
265266
queryType: 'regularQuery'
@@ -272,6 +273,113 @@ describe('API Gateway', () => {
272273
order: [],
273274
filters: [],
274275
rowLimit: 10000,
276+
limit: 10000,
277+
dimensions: [],
278+
timeDimensions: [],
279+
queryType: 'regularQuery'
280+
},
281+
transformedQueries: [null]
282+
});
283+
}
284+
);
285+
});
286+
287+
test('normalize queryRewrite limit', async () => {
288+
const { app } = createApiGateway(
289+
new AdapterApiMock(),
290+
new DataSourceStorageMock(),
291+
{
292+
checkAuth: (req: Request, authorization) => {
293+
if (authorization) {
294+
jwt.verify(authorization, API_SECRET);
295+
req.authInfo = authorization;
296+
}
297+
},
298+
queryRewrite: async (query, context) => {
299+
query.limit = 2;
300+
return query;
301+
}
302+
}
303+
);
304+
305+
const query = {
306+
measures: ['Foo.bar']
307+
};
308+
309+
return requestBothGetAndPost(
310+
app,
311+
{ url: '/cubejs-api/v1/dry-run', query: { query: JSON.stringify(query) }, body: { query } },
312+
(res) => {
313+
expect(res.body).toStrictEqual({
314+
queryType: 'regularQuery',
315+
normalizedQueries: [
316+
{
317+
measures: ['Foo.bar'],
318+
timezone: 'UTC',
319+
order: [],
320+
filters: [],
321+
rowLimit: 2,
322+
limit: 2,
323+
dimensions: [],
324+
timeDimensions: [],
325+
queryType: 'regularQuery'
326+
}
327+
],
328+
queryOrder: [{ id: 'desc' }],
329+
pivotQuery: {
330+
measures: ['Foo.bar'],
331+
timezone: 'UTC',
332+
order: [],
333+
filters: [],
334+
rowLimit: 2,
335+
limit: 2,
336+
dimensions: [],
337+
timeDimensions: [],
338+
queryType: 'regularQuery'
339+
},
340+
transformedQueries: [null]
341+
});
342+
}
343+
);
344+
});
345+
346+
test('normalize order', async () => {
347+
const { app } = createApiGateway();
348+
349+
const query = {
350+
measures: ['Foo.bar'],
351+
order: {
352+
'Foo.bar': 'desc'
353+
}
354+
};
355+
356+
return requestBothGetAndPost(
357+
app,
358+
{ url: '/cubejs-api/v1/dry-run', query: { query: JSON.stringify(query) }, body: { query } },
359+
(res) => {
360+
expect(res.body).toStrictEqual({
361+
queryType: 'regularQuery',
362+
normalizedQueries: [
363+
{
364+
measures: ['Foo.bar'],
365+
order: [{ id: 'Foo.bar', desc: true }],
366+
timezone: 'UTC',
367+
filters: [],
368+
rowLimit: 10000,
369+
limit: 10000,
370+
dimensions: [],
371+
timeDimensions: [],
372+
queryType: 'regularQuery'
373+
}
374+
],
375+
queryOrder: [{ id: 'desc' }],
376+
pivotQuery: {
377+
measures: ['Foo.bar'],
378+
order: [{ id: 'Foo.bar', desc: true }],
379+
timezone: 'UTC',
380+
filters: [],
381+
rowLimit: 10000,
382+
limit: 10000,
275383
dimensions: [],
276384
timeDimensions: [],
277385
queryType: 'regularQuery'

0 commit comments

Comments
 (0)