Skip to content

Commit db41f2b

Browse files
feat: Adding support for having clause (#56)
* having support added * reverted test case changes * reusing traverse and filter * removed unneeded function * code refactor * bumped up versions * fixed test cases * fixed test cases * self review * added test case for having/where filter
1 parent 1dd17d4 commit db41f2b

File tree

13 files changed

+406
-135
lines changed

13 files changed

+406
-135
lines changed

meerkat-core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devrev/meerkat-core",
3-
"version": "0.0.62",
3+
"version": "0.0.63",
44
"dependencies": {
55
"@swc/helpers": "~0.5.0"
66
},

meerkat-core/src/ast-builder/ast-builder.ts

+46-16
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,44 @@ import { cubeFilterToDuckdbAST } from '../cube-filter-transformer/factory';
22
import { cubeDimensionToGroupByAST } from '../cube-group-by-transformer/cube-group-by-transformer';
33
import { cubeLimitOffsetToAST } from '../cube-limit-offset-transformer/cube-limit-offset-transformer';
44
import { cubeOrderByToAST } from '../cube-order-by-transformer/cube-order-by-transformer';
5-
import { QueryFiltersWithInfo } from '../cube-to-duckdb/cube-filter-to-duckdb';
6-
import { FilterType, Query } from '../types/cube-types/query';
5+
import { QueryFiltersWithInfo, QueryFiltersWithInfoSingular } from '../cube-to-duckdb/cube-filter-to-duckdb';
6+
import { traverseAndFilter } from '../filter-params/filter-params-ast';
7+
import { FilterType, MeerkatQueryFilter, Query } from '../types/cube-types/query';
78
import { TableSchema } from '../types/cube-types/table';
9+
import { SelectStatement } from '../types/duckdb-serialization-types';
810
import { SelectNode } from '../types/duckdb-serialization-types/serialization/QueryNode';
911
import { getBaseAST } from '../utils/base-ast';
1012
import { cubeFiltersEnrichment } from '../utils/cube-filter-enrichment';
1113
import { modifyLeafMeerkatFilter } from '../utils/modify-meerkat-filter';
1214

1315

16+
const formatFilters = (queryFiltersWithInfo: QueryFiltersWithInfo, filterType?: FilterType) => {
17+
/*
18+
* If the type of filter is set to base filter where
19+
*/
20+
return filterType === 'BASE_FILTER' ? queryFiltersWithInfo : modifyLeafMeerkatFilter(queryFiltersWithInfo, (item) => {
21+
return {
22+
...item,
23+
member: item.member.split('.').join('__')
24+
};
25+
}) as QueryFiltersWithInfo;
26+
}
27+
28+
29+
const getFormattedFilters = ({ queryFiltersWithInfo, filterType, mapperFn, baseAST }: {
30+
queryFiltersWithInfo: QueryFiltersWithInfo,
31+
filterType?: FilterType,
32+
baseAST: SelectStatement,
33+
mapperFn: (val: QueryFiltersWithInfoSingular) => MeerkatQueryFilter | null
34+
}) => {
35+
const filters = queryFiltersWithInfo.map(item => mapperFn(item)).filter(Boolean) as QueryFiltersWithInfoSingular[];
36+
const formattedFilters = formatFilters(filters, filterType);
37+
return cubeFilterToDuckdbAST(
38+
formattedFilters,
39+
baseAST
40+
);
41+
}
42+
1443
export const cubeToDuckdbAST = (query: Query, tableSchema: TableSchema, options?: { filterType: FilterType }
1544
) => {
1645
/**
@@ -35,23 +64,24 @@ export const cubeToDuckdbAST = (query: Query, tableSchema: TableSchema, options?
3564
return null;
3665
}
3766

38-
/*
39-
* If the type of filter is set to base filter where
40-
*/
41-
const finalFilters = options?.filterType === 'BASE_FILTER' ? queryFiltersWithInfo : modifyLeafMeerkatFilter(queryFiltersWithInfo, (item) => {
42-
return {
43-
...item,
44-
member: item.member.split('.').join('__')
45-
};
46-
}) as QueryFiltersWithInfo;
67+
const whereClause = getFormattedFilters({
68+
baseAST,
69+
mapperFn: (item) => traverseAndFilter(item, (value) => !query.measures.includes(value.member)),
70+
queryFiltersWithInfo,
71+
filterType: options?.filterType
72+
})
73+
74+
const havingClause = getFormattedFilters({
75+
baseAST,
76+
mapperFn: (item) => traverseAndFilter(item, (value) => query.measures.includes(value.member)),
77+
queryFiltersWithInfo,
78+
filterType: options?.filterType
79+
})
4780

48-
const duckdbWhereClause = cubeFilterToDuckdbAST(
49-
finalFilters,
50-
baseAST
51-
);
5281
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5382
//@ts-ignore
54-
node.where_clause = duckdbWhereClause;
83+
node.where_clause = whereClause;
84+
node.having = havingClause
5585
}
5686

5787
if (query.dimensions && query.dimensions?.length > 0) {

meerkat-core/src/cube-to-duckdb/cube-filter-to-duckdb.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Dimension, Measure } from '../types/cube-types/table';
21
import { QueryFilter } from '../types/cube-types/query';
2+
import { Dimension, Measure } from '../types/cube-types/table';
33

44
export type QueryOperatorsWithInfo = QueryFilter & {
55
memberInfo: Measure | Dimension;
@@ -28,8 +28,8 @@ export type QueryFilterWithInfo =
2828
| LogicalOrFilterWithInfo
2929
)[];
3030

31-
export type QueryFiltersWithInfo = (
32-
| QueryOperatorsWithInfo
33-
| LogicalAndFilterWithInfo
34-
| LogicalOrFilterWithInfo
35-
)[];
31+
export type QueryFiltersWithInfoSingular = QueryOperatorsWithInfo
32+
| LogicalAndFilterWithInfo
33+
| LogicalOrFilterWithInfo;
34+
35+
export type QueryFiltersWithInfo = QueryFiltersWithInfoSingular[];

meerkat-core/src/filter-params/filter-params-ast.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@ import {
55
LogicalOrFilter,
66
MeerkatQueryFilter,
77
Query,
8+
QueryFilter,
89
TableSchema
910
} from '../types/cube-types';
1011
import { SelectStatement } from '../types/duckdb-serialization-types/serialization/Statement';
1112

1213
/**
1314
* Get the query filter with only where filterKey matches
1415
*/
15-
const traverseAndFilter = (
16+
17+
export const traverseAndFilter = (
1618
filter: MeerkatQueryFilter,
17-
memberKey: string
19+
callback: (value: QueryFilter) => boolean
1820
): MeerkatQueryFilter | null => {
1921
if ('member' in filter) {
20-
return filter.member === memberKey ? filter : null;
22+
return callback(filter) ? filter : null;
2123
}
2224

2325
if ('and' in filter) {
2426
const filteredAndFilters = filter.and
25-
.map((subFilter) => traverseAndFilter(subFilter, memberKey))
27+
.map((subFilter) => traverseAndFilter(subFilter, callback))
2628
.filter(Boolean) as MeerkatQueryFilter[];
2729
const obj =
2830
filteredAndFilters.length > 0 ? { and: filteredAndFilters } : null;
@@ -31,7 +33,7 @@ const traverseAndFilter = (
3133

3234
if ('or' in filter) {
3335
const filteredOrFilters = filter.or
34-
.map((subFilter) => traverseAndFilter(subFilter, memberKey))
36+
.map((subFilter) => traverseAndFilter(subFilter, callback))
3537
.filter(Boolean);
3638
const obj = filteredOrFilters.length > 0 ? { or: filteredOrFilters } : null;
3739
return obj as LogicalOrFilter;
@@ -47,7 +49,7 @@ export const getFilterByMemberKey = (
4749
): MeerkatQueryFilter[] => {
4850
if (!filters) return [];
4951
return filters
50-
.map((filter) => traverseAndFilter(filter, memberKey))
52+
.map((filter) => traverseAndFilter(filter, (value) => value.member === memberKey))
5153
.filter(Boolean) as MeerkatQueryFilter[];
5254
};
5355

meerkat-core/src/get-projection-clause/get-projection-clause.spec.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
1-
import { get } from "http";
21
import { TableSchema } from "../types/cube-types";
3-
import { getMemberProjection, getProjectionClause } from './get-projection-clause';
2+
import { getDimensionProjection, getFilterMeasureProjection, getProjectionClause } from './get-projection-clause';
43

54

65
const TABLE_SCHEMA: TableSchema = {
76
dimensions: [{ name: 'a', sql: 'others', type: 'number' }, { name: 'c', sql: 'any', type: 'string' }],
8-
measures: [],
7+
measures: [{ name: 'x', sql: 'x', type: 'number' }, { name: 'y', sql: 'y', type: 'number' }, { name: 'z', sql: 'z', type: 'number' }],
98
name: 'test',
109
sql: 'SELECT * from test'
1110
// Define your table schema here
1211
};
1312
describe("get-projection-clause", () => {
14-
describe("getMemberProjection", () => {
13+
describe("getDimensionProjection", () => {
1514
it("should return the member projection when the key exists in the table schema", () => {
1615
const key = "test.a";
17-
1816

19-
const result = getMemberProjection({ key, tableSchema: TABLE_SCHEMA });
17+
const result = getDimensionProjection({ key, tableSchema: TABLE_SCHEMA });
2018
expect(result).toEqual({ aliasKey: "test__a", foundMember: {"name": "a", "sql": "others", "type": "number"}, sql: "others AS test__a"});
2119
});
2220

@@ -27,25 +25,48 @@ describe("get-projection-clause", () => {
2725
dimensions: [{ name: 'b', sql: 'others', type: 'number' }],
2826
};
2927

30-
const result = getMemberProjection({ key, tableSchema });
28+
const result = getDimensionProjection({ key, tableSchema });
29+
expect(result).toEqual({ aliasKey: undefined, foundMember: undefined, sql: undefined });
30+
});
31+
})
32+
33+
describe("getFilterMeasureProjection", () => {
34+
it("should return the member projection when the key exists in the table schema", () => {
35+
const key = "test.x";
36+
const result = getFilterMeasureProjection({ key, tableSchema: TABLE_SCHEMA, measures: ['test.a']});
37+
expect(result).toEqual({ aliasKey: "test__x", foundMember: {"name": "x", "sql": "x", "type": "number"}, sql: "test.x AS test__x"});
38+
});
39+
40+
it("should not create alias when item in measure list", () => {
41+
const key = "test.x";
42+
const result = getFilterMeasureProjection({ key, tableSchema: TABLE_SCHEMA, measures: ['test.x']});
43+
expect(result).toEqual({ aliasKey: undefined, foundMember: undefined, sql: undefined});
44+
});
45+
46+
it("should return the object with undefined values when the key doesn't exist in the table schema", () => {
47+
const key = "test.a";
48+
const tableSchema: TableSchema = {
49+
...TABLE_SCHEMA,
50+
measures: [{ name: 'b', sql: 'others', type: 'number' }],
51+
};
52+
53+
const result = getFilterMeasureProjection({ key, tableSchema, measures: ['test.b'] });
3154
expect(result).toEqual({ aliasKey: undefined, foundMember: undefined, sql: undefined });
3255
});
3356
})
3457

3558
describe("getProjectionClause", () => {
3659
it('should return the projection clause when the members are present in the table schema', () => {
3760
const members = ['test.a', 'test.c'];
38-
const tableSchema = TABLE_SCHEMA;
3961
const aliasedColumnSet = new Set<string>();
40-
const result = getProjectionClause(members, tableSchema, aliasedColumnSet);
41-
expect(result).toEqual(', others AS test__a, any AS test__c');
62+
const result = getProjectionClause([], members, TABLE_SCHEMA, aliasedColumnSet);
63+
expect(result).toEqual('others AS test__a, any AS test__c');
4264
})
4365
it('should skip aliased items present in already seen', () => {
4466
const members = ['test.a', 'test.c'];
45-
const tableSchema = TABLE_SCHEMA;
4667
const aliasedColumnSet = new Set<string>(['test.c']);
47-
const result = getProjectionClause(members, tableSchema, aliasedColumnSet);
48-
expect(result).toEqual(', others AS test__a');
68+
const result = getProjectionClause([], members, TABLE_SCHEMA, aliasedColumnSet);
69+
expect(result).toEqual('others AS test__a, ');
4970
})
5071
})
5172

Original file line numberDiff line numberDiff line change
@@ -1,36 +1,85 @@
11
import { TableSchema } from "../types/cube-types";
2-
import { findInSchema } from "../utils/find-in-table-schema";
2+
import { findInDimensionSchema, findInMeasureSchema } from "../utils/find-in-table-schema";
33
import { memberKeyToSafeKey } from "../utils/member-key-to-safe-key";
44

5-
export const getMemberProjection = ({ key, tableSchema }: {
6-
key: string;
7-
tableSchema: TableSchema;
8-
}) => {
9-
// Find the table access key
10-
const measureWithoutTable = key.split('.')[1];
11-
12-
const foundMember = findInSchema(measureWithoutTable, tableSchema)
13-
if (!foundMember) {
14-
// If the selected member is not found in the table schema or if it is already selected, continue.
15-
return {
16-
sql: undefined,
17-
foundMember: undefined,
18-
aliasKey: undefined
19-
}
5+
6+
export const getFilterMeasureProjection = ({ key, tableSchema, measures }: {
7+
key: string;
8+
tableSchema: TableSchema;
9+
measures: string[]
10+
}) => {
11+
const measureWithoutTable = key.split('.')[1];
12+
const foundMember = findInMeasureSchema(measureWithoutTable, tableSchema);
13+
const isMeasure = measures.includes(key);
14+
if (!foundMember || isMeasure) {
15+
// If the selected member is not found in the table schema or if it is already selected, continue.
16+
// If the selected member is a measure, don't create an alias. Since measure computation is done in the outermost level of the query
17+
return {
18+
sql: undefined,
19+
foundMember: undefined,
20+
aliasKey: undefined
2021
}
21-
const aliasKey = memberKeyToSafeKey(key);
22-
// Add the alias key to the set. So we have a reference to all the previously selected members.
23-
return { sql: `${foundMember.sql} AS ${aliasKey}` , foundMember, aliasKey }
22+
}
23+
const aliasKey = memberKeyToSafeKey(key);
24+
return { sql: `${key} AS ${aliasKey}` , foundMember, aliasKey }
2425
}
2526

27+
export const getDimensionProjection = ({ key, tableSchema }: {
28+
key: string;
29+
tableSchema: TableSchema;
30+
}) => {
31+
// Find the table access key
32+
const measureWithoutTable = key.split('.')[1];
2633

27-
export const getProjectionClause = (members: string[], tableSchema: TableSchema, aliasedColumnSet: Set<string>) => {
28-
return members.reduce((acc, member) => {
29-
const { sql: memberSql } = getMemberProjection({ key: member, tableSchema })
30-
if (aliasedColumnSet.has(member)) {
31-
return acc
34+
const foundMember = findInDimensionSchema(measureWithoutTable, tableSchema)
35+
if (!foundMember) {
36+
// If the selected member is not found in the table schema or if it is already selected, continue.
37+
return {
38+
sql: undefined,
39+
foundMember: undefined,
40+
aliasKey: undefined
3241
}
33-
acc += `, ${memberSql}`
42+
}
43+
const aliasKey = memberKeyToSafeKey(key);
44+
// Add the alias key to the set. So we have a reference to all the previously selected members.
45+
return { sql: `${foundMember.sql} AS ${aliasKey}` , foundMember, aliasKey }
46+
}
47+
48+
const aggregator = ({
49+
member,
50+
aliasedColumnSet,
51+
acc,
52+
currentIndex,
53+
members,
54+
sql
55+
}: {
56+
member: string;
57+
aliasedColumnSet: Set<string>;
58+
acc: string;
59+
sql?: string;
60+
currentIndex: number,
61+
members: string[]
62+
}) => {
63+
if (aliasedColumnSet.has(member) || !sql) {
3464
return acc
65+
}
66+
aliasedColumnSet.add(member)
67+
acc += sql
68+
if (currentIndex !== members.length - 1) {
69+
acc += `, `
70+
}
71+
return acc
72+
}
73+
74+
75+
export const getProjectionClause = (measures: string[], dimensions: string[], tableSchema: TableSchema, aliasedColumnSet: Set<string>) => {
76+
const dimensionsProjections = dimensions.reduce((acc, member, currentIndex, members) => {
77+
const { sql: memberSql } = getDimensionProjection({ key: member, tableSchema })
78+
return aggregator({ member, aliasedColumnSet, acc, currentIndex, members, sql: memberSql })
3579
}, '')
36-
}
80+
const measureProjections = measures.reduce((acc, member, currentIndex, members) => {
81+
const { sql: memberSql } = getFilterMeasureProjection({ key: member, tableSchema, measures })
82+
return aggregator({ member, aliasedColumnSet, acc, currentIndex, members, sql: memberSql })
83+
}, '')
84+
return dimensionsProjections + (dimensionsProjections.length && measureProjections.length ? ', ' : '') + measureProjections
85+
}

0 commit comments

Comments
 (0)