Skip to content

Commit 4c7fde5

Browse files
authored
feat(cubesql): Ungrouped SQL push down (#7102)
* feat(cubesql): Ungrouped SQL push down * Match members instead of columns for push down * Fix tests * Dimension push down rules * Dimension push down rules * Move view evaluation to Cube Symbols so it's accessible in member expressions * Use evaluateSymbolSql for member expressions to capture it's dependencies and allow full key query aggregate queries * Fix warnings and add TODOs * Add more TODOs
1 parent 233cb6d commit 4c7fde5

File tree

27 files changed

+1565
-498
lines changed

27 files changed

+1565
-498
lines changed

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
} from './types/auth';
5050
import {
5151
Query,
52-
NormalizedQuery,
52+
NormalizedQuery, MemberExpression,
5353
} from './types/query';
5454
import {
5555
UserBackgroundContext,
@@ -89,6 +89,8 @@ import {
8989
transformPreAggregations,
9090
} from './helpers/transformMetaExtended';
9191

92+
const memberExpressionRegex = /^([a-zA-Z0-9_]+).([a-zA-Z0-9_]+):\(([a-zA-Z0-9_,]+)\):(.*)$/;
93+
9294
/**
9395
* API gateway server class.
9496
*/
@@ -1170,18 +1172,23 @@ class ApiGateway {
11701172
return [queryType, normalizedQueries];
11711173
}
11721174

1173-
public async sql({ query, context, res, memberToAlias, exportAnnotatedSql }: QueryRequest) {
1175+
public async sql({ query, context, res, memberToAlias, exportAnnotatedSql, memberExpressions, expressionParams }: QueryRequest) {
11741176
const requestStarted = new Date();
11751177

11761178
try {
11771179
await this.assertApiScope('data', context.securityContext);
11781180

11791181
query = this.parseQueryParam(query);
1182+
1183+
if (memberExpressions) {
1184+
query = this.parseMemberExpressionsInQueries(query);
1185+
}
1186+
11801187
const [queryType, normalizedQueries] = await this.getNormalizedQueries(query, context);
11811188

11821189
const sqlQueries = await Promise.all<any>(
11831190
normalizedQueries.map(async (normalizedQuery) => (await this.getCompilerApi(context)).getSql(
1184-
this.coerceForSqlQuery({ ...normalizedQuery, memberToAlias }, context),
1191+
this.coerceForSqlQuery({ ...normalizedQuery, memberToAlias, expressionParams }, context),
11851192
{
11861193
includeDebugInfo: getEnv('devMode') || context.signedWithPlaygroundAuthSecret,
11871194
exportAnnotatedSql,
@@ -1204,6 +1211,39 @@ class ApiGateway {
12041211
}
12051212
}
12061213

1214+
private parseMemberExpressionsInQueries(query: Record<string, any> | Record<string, any>[]): Query | Query[] {
1215+
if (Array.isArray(query)) {
1216+
return query.map(q => this.parseMemberExpressionsInQuery(<Query>q));
1217+
} else {
1218+
return this.parseMemberExpressionsInQuery(<Query>query);
1219+
}
1220+
}
1221+
1222+
private parseMemberExpressionsInQuery(query: Query): Query {
1223+
return {
1224+
...query,
1225+
measures: (query.measures || []).map(m => (typeof m === 'string' ? this.parseMemberExpression(m) : m)),
1226+
dimensions: (query.dimensions || []).map(m => (typeof m === 'string' ? this.parseMemberExpression(m) : m)),
1227+
};
1228+
}
1229+
1230+
private parseMemberExpression(memberExpression: string): string | MemberExpression {
1231+
const match = memberExpression.match(memberExpressionRegex);
1232+
if (match) {
1233+
const args = match[3].split(',');
1234+
args.push(`return \`${match[4]}\``);
1235+
return {
1236+
cubeName: match[1],
1237+
name: match[2],
1238+
expressionName: match[2],
1239+
expression: Function.constructor.apply(null, args),
1240+
definition: memberExpression,
1241+
};
1242+
} else {
1243+
return memberExpression;
1244+
}
1245+
}
1246+
12071247
public async sqlGenerators({ context, res }: { context: RequestContext, res: ResponseResultFn }) {
12081248
const requestStarted = new Date();
12091249

@@ -1651,6 +1691,8 @@ class ApiGateway {
16511691
query = this.parseQueryParam(request.query);
16521692
let resType: ResultType = ResultType.DEFAULT;
16531693

1694+
query = this.parseMemberExpressionsInQueries(query);
1695+
16541696
if (!Array.isArray(query) && query.responseFormat) {
16551697
resType = query.responseFormat;
16561698
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import R from 'ramda';
99
import { MetaConfig, MetaConfigMap, toConfigMap } from './toConfigMap';
1010
import { MemberType } from '../types/strings';
1111
import { MemberType as MemberTypeEnum } from '../types/enums';
12+
import { MemberExpression } from '../types/query';
1213

1314
/**
1415
* Annotation item for cube's member.
@@ -30,16 +31,16 @@ type ConfigItem = {
3031
const annotation = (
3132
configMap: MetaConfigMap,
3233
memberType: MemberType,
33-
) => (member: string): undefined | [string, ConfigItem] => {
34-
const [cubeName, fieldName] = member.split('.');
34+
) => (member: string | MemberExpression): undefined | [string, ConfigItem] => {
35+
const [cubeName, fieldName] = (<MemberExpression>member).expression ? [(<MemberExpression>member).cubeName, (<MemberExpression>member).name] : (<string>member).split('.');
3536
const memberWithoutGranularity = [cubeName, fieldName].join('.');
3637
const config: ConfigItem = configMap[cubeName][memberType]
3738
.find(m => m.name === memberWithoutGranularity);
3839

3940
if (!config) {
4041
return undefined;
4142
}
42-
return [member, {
43+
return [typeof member === 'string' ? member : memberWithoutGranularity, {
4344
title: config.title,
4445
shortTitle: config.shortTitle,
4546
description: config.description,

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ const getPivotQuery = (queryType, queries) => {
4040

4141
const id = Joi.string().regex(/^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+$/);
4242
const dimensionWithTime = Joi.string().regex(/^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+(\.(second|minute|hour|day|week|month|year))?$/);
43+
const memberExpression = Joi.object().keys({
44+
expression: Joi.func().required(),
45+
cubeName: Joi.string().required(),
46+
name: Joi.string().required(),
47+
expressionName: Joi.string(),
48+
definition: Joi.string(),
49+
});
4350

4451
const operators = [
4552
'equals',
@@ -79,8 +86,9 @@ const oneCondition = Joi.object().keys({
7986
}).xor('or', 'and');
8087

8188
const querySchema = Joi.object().keys({
82-
measures: Joi.array().items(id),
83-
dimensions: Joi.array().items(dimensionWithTime),
89+
// TODO add member expression alternatives only for SQL API queries?
90+
measures: Joi.array().items(Joi.alternatives(id, memberExpression)),
91+
dimensions: Joi.array().items(Joi.alternatives(dimensionWithTime, memberExpression)),
8492
filters: Joi.array().items(oneFilter, oneCondition),
8593
timeDimensions: Joi.array().items(Joi.object().keys({
8694
dimension: id.required(),
@@ -176,7 +184,7 @@ const normalizeQuery = (query, persistent) => {
176184
);
177185
}
178186

179-
const regularToTimeDimension = (query.dimensions || []).filter(d => d.split('.').length === 3).map(d => ({
187+
const regularToTimeDimension = (query.dimensions || []).filter(d => typeof d === 'string' && d.split('.').length === 3).map(d => ({
180188
dimension: d.split('.').slice(0, 2).join('.'),
181189
granularity: d.split('.')[2]
182190
}));
@@ -207,7 +215,7 @@ const normalizeQuery = (query, persistent) => {
207215
timezone,
208216
order: normalizeQueryOrder(query.order),
209217
filters: normalizeQueryFilters(query.filters || []),
210-
dimensions: (query.dimensions || []).filter(d => d.split('.').length !== 3),
218+
dimensions: (query.dimensions || []).filter(d => typeof d !== 'string' || d.split('.').length !== 3),
211219
timeDimensions: (query.timeDimensions || []).map(td => {
212220
let dateRange;
213221

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class SQLServer {
136136
}
137137
});
138138
},
139-
sql: async ({ request, session, query, memberToAlias }) => {
139+
sql: async ({ request, session, query, memberToAlias, expressionParams }) => {
140140
const context = await contextByRequest(request, session);
141141

142142
// eslint-disable-next-line no-async-promise-executor
@@ -145,7 +145,9 @@ export class SQLServer {
145145
await this.apiGateway.sql({
146146
query,
147147
memberToAlias,
148+
expressionParams,
148149
exportAnnotatedSql: true,
150+
memberExpressions: true,
149151
queryType: 'multi',
150152
context,
151153
res: (message) => {

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ type LogicalOrFilter = {
3939
or: (QueryFilter | LogicalAndFilter)[]
4040
};
4141

42+
type MemberExpression = {
43+
expression: Function;
44+
cubeName: string;
45+
name: string;
46+
expressionName: string;
47+
definition: string;
48+
};
49+
4250
/**
4351
* Query datetime dimention interface.
4452
*/
@@ -52,8 +60,8 @@ interface QueryTimeDimension {
5260
* Incoming network query data type.
5361
*/
5462
interface Query {
55-
measures: Member[];
56-
dimensions?: (Member | TimeMember)[];
63+
measures: (Member | MemberExpression)[];
64+
dimensions?: (Member | TimeMember | MemberExpression)[];
5765
filters?: (QueryFilter | LogicalAndFilter | LogicalOrFilter)[];
5866
timeDimensions?: QueryTimeDimension[];
5967
segments?: Member[];
@@ -92,4 +100,5 @@ export {
92100
Query,
93101
NormalizedQueryFilter,
94102
NormalizedQuery,
103+
MemberExpression,
95104
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ type QueryRequest = BaseRequest & {
118118
apiType?: ApiType;
119119
resType?: ResultType
120120
memberToAlias?: Record<string, string>;
121+
expressionParams?: string[];
121122
exportAnnotatedSql?: boolean;
123+
memberExpressions?: boolean;
122124
};
123125

124126
type SqlApiRequest = BaseRequest & {

packages/cubejs-backend-native/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface SqlPayload {
4848
session: SessionContext,
4949
query: any,
5050
memberToAlias: Record<string, string>,
51+
expressionParams: string[],
5152
}
5253

5354
export interface SqlApiLoadPayload {

packages/cubejs-backend-native/src/transport.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ struct LoadRequest {
6868
session: SessionContext,
6969
#[serde(rename = "memberToAlias", skip_serializing_if = "Option::is_none")]
7070
member_to_alias: Option<HashMap<String, String>>,
71+
#[serde(rename = "expressionParams", skip_serializing_if = "Option::is_none")]
72+
expression_params: Option<Vec<Option<String>>>,
7173
streaming: bool,
7274
}
7375

@@ -176,6 +178,7 @@ impl TransportService for NodeBridgeTransport {
176178
ctx: AuthContextRef,
177179
meta: LoadRequestMeta,
178180
member_to_alias: Option<HashMap<String, String>>,
181+
expression_params: Option<Vec<Option<String>>>,
179182
) -> Result<SqlResponse, CubeError> {
180183
let native_auth = ctx
181184
.as_any()
@@ -196,6 +199,7 @@ impl TransportService for NodeBridgeTransport {
196199
},
197200
sql_query: None,
198201
member_to_alias,
202+
expression_params,
199203
streaming: false,
200204
})?;
201205

@@ -269,6 +273,7 @@ impl TransportService for NodeBridgeTransport {
269273
},
270274
sql_query: sql_query.clone().map(|q| (q.sql, q.values)),
271275
member_to_alias: None,
276+
expression_params: None,
272277
streaming: false,
273278
})?;
274279

@@ -345,6 +350,7 @@ impl TransportService for NodeBridgeTransport {
345350
superuser: native_auth.superuser,
346351
},
347352
member_to_alias: None,
353+
expression_params: None,
348354
streaming: true,
349355
})?;
350356

packages/cubejs-schema-compiler/src/adapter/BaseDimension.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
export class BaseDimension {
22
constructor(query, dimension) {
33
this.query = query;
4+
if (dimension && dimension.expression) {
5+
this.expression = dimension.expression;
6+
this.expressionCubeName = dimension.cubeName;
7+
this.expressionName = dimension.expressionName || `${dimension.cubeName}.${dimension.name}`;
8+
this.isMemberExpression = !!dimension.definition;
9+
}
410
this.dimension = dimension;
511
}
612

@@ -17,6 +23,9 @@ export class BaseDimension {
1723
}
1824

1925
dimensionSql() {
26+
if (this.expression) {
27+
return this.query.evaluateSymbolSql(this.expressionCubeName, this.expressionName, this.definition(), 'dimension');
28+
}
2029
if (this.query.cubeEvaluator.isSegment(this.dimension)) {
2130
return this.query.wrapSegmentForDimensionSelect(this.query.dimensionSql(this));
2231
}
@@ -32,6 +41,9 @@ export class BaseDimension {
3241
}
3342

3443
cube() {
44+
if (this.expression) {
45+
return this.query.cubeEvaluator.cubeFromPath(this.expressionCubeName);
46+
}
3547
return this.query.cubeEvaluator.cubeFromPath(this.dimension);
3648
}
3749

@@ -43,6 +55,13 @@ export class BaseDimension {
4355
}
4456

4557
definition() {
58+
if (this.expression) {
59+
return {
60+
sql: this.expression,
61+
// TODO use actual dimension type even though it isn't used right now
62+
type: 'number'
63+
};
64+
}
4665
return this.dimensionDefinition();
4766
}
4867

@@ -52,6 +71,9 @@ export class BaseDimension {
5271
}
5372

5473
unescapedAliasName() {
74+
if (this.expression) {
75+
return this.query.aliasName(this.expressionName);
76+
}
5577
return this.query.aliasName(this.dimension);
5678
}
5779

@@ -60,6 +82,9 @@ export class BaseDimension {
6082
}
6183

6284
path() {
85+
if (this.expression) {
86+
return null;
87+
}
6388
if (this.query.cubeEvaluator.isSegment(this.dimension)) {
6489
return this.query.cubeEvaluator.parsePath('segments', this.dimension);
6590
}

packages/cubejs-schema-compiler/src/adapter/BaseMeasure.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export class BaseMeasure {
66
if (measure.expression) {
77
this.expression = measure.expression;
88
this.expressionCubeName = measure.cubeName;
9-
this.expressionName = `${measure.cubeName}.${measure.name}`;
9+
this.expressionName = measure.expressionName || `${measure.cubeName}.${measure.name}`;
10+
this.isMemberExpression = !!measure.definition;
1011
}
1112
this.measure = measure;
1213
}
@@ -38,7 +39,7 @@ export class BaseMeasure {
3839

3940
measureSql() {
4041
if (this.expression) {
41-
return this.query.evaluateSql(this.expressionCubeName, this.expression);
42+
return this.query.evaluateSymbolSql(this.expressionCubeName, this.expressionName, this.definition(), 'measure');
4243
}
4344
return this.query.measureSql(this);
4445
}
@@ -58,6 +59,7 @@ export class BaseMeasure {
5859
if (this.expression) {
5960
return {
6061
sql: this.expression,
62+
// TODO use actual measure type even though it isn't used right now
6163
type: 'number'
6264
};
6365
}

0 commit comments

Comments
 (0)