Skip to content

Commit

Permalink
Group by common table expressions support WiP
Browse files Browse the repository at this point in the history
  • Loading branch information
ad-elias committed Aug 18, 2024
1 parent e41dc25 commit f8e591f
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
import { z } from 'zod';

const chartQuerySchema = z
.object({
sourceObjectMetadataId: z.string().nullable(),
target: z
.object({
relationFieldMetadataIds: z.array(z.string()).nullable(),
measureFieldMetadataId: z.string().nullable(),
measure: z.string().nullable(),
})
.partial(),
groupBy: z
.object({
relationFieldMetadataIds: z.array(z.string()).nullable(),
measureFieldMetadataId: z.string().nullable(),
measure: z.string().nullable(),
groups: z
.array(
z
.object({ upperLimit: z.number(), lowerLimit: z.number() })
.partial(),
)
.nullable(),
includeNulls: z.boolean().nullable(),
})
.partial(),
// Later: Filters should be included in the CHART_QUERY UI => better to also store them in CHART_QUERY JSON?
})
.partial();

export type ChartQuery = z.infer<typeof chartQuerySchema>;

const fieldPathSchema = z.array(z.string()).nullable();

export const isFieldFieldPathValue = (
fieldValue: unknown,
): fieldValue is FieldJsonValue =>
z.array(z.string()).nullable().safeParse(fieldValue).success;
fieldPathSchema.safeParse(fieldValue).success;
121 changes: 70 additions & 51 deletions packages/twenty-server/src/engine/core-modules/chart/chart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Repository } from 'typeorm';

import { ChartResult } from 'src/engine/core-modules/chart/dtos/chart-result.dto';
import { AliasPrefix } from 'src/engine/core-modules/chart/types/alias-prefix.type';
import { ChartQuery } from 'src/engine/core-modules/chart/types/chart-query';
import { CommonTableExpressionDefinition } from 'src/engine/core-modules/chart/types/common-table-expression-definition.type';
import { QueryRelation } from 'src/engine/core-modules/chart/types/query-relation.type';
import {
Expand All @@ -25,7 +26,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import {
ChartMeasure,
ChartQueryMeasure,
ChartWorkspaceEntity,
} from 'src/modules/charts/standard-objects/chart.workspace-entity';

Expand Down Expand Up @@ -219,54 +220,56 @@ export class ChartService {
}

private getMeasureSelectColumn(
chartMeasure: ChartMeasure,
chartMeasure: ChartQueryMeasure,
targetQualifiedColumn: string,
) {
if (!targetQualifiedColumn && chartMeasure !== ChartMeasure.COUNT) {
if (!targetQualifiedColumn && chartMeasure !== ChartQueryMeasure.COUNT) {
throw new Error(
'Chart measure must be count when target column is undefined',
);
}

switch (chartMeasure) {
case ChartMeasure.COUNT:
case ChartQueryMeasure.COUNT:
return 'COUNT(*) as measure';
case ChartMeasure.AVERAGE:
case ChartQueryMeasure.AVERAGE:
return `AVG(${targetQualifiedColumn}) as measure`;
case ChartMeasure.MIN:
case ChartQueryMeasure.MIN:
return `MIN(${targetQualifiedColumn}) as measure`;
case ChartMeasure.MAX:
case ChartQueryMeasure.MAX:
return `MAX(${targetQualifiedColumn}) as measure`;
case ChartMeasure.SUM:
case ChartQueryMeasure.SUM:
return `SUM(${targetQualifiedColumn}) as measure`;
}
}

private async getFieldMetadata(workspaceId, fieldMetadataId) {
return await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
id: fieldMetadataId,
},
});
return (
(await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
id: fieldMetadataId,
},
})) ?? undefined
);
}

private async getQualifiedColumn(
workspaceId: string,
targetQueryRelations: QueryRelation[],
sourceTableName: string,
targetRelationFieldMetadataIds: string[],
targetMeasureFieldMetadata?: FieldMetadataEntity,
relationFieldMetadataIds?: string[],
measureFieldMetadata?: FieldMetadataEntity,
) {
const lastTargetRelationFieldMetadataId =
targetRelationFieldMetadataIds[targetRelationFieldMetadataIds.length - 1];
relationFieldMetadataIds?.[relationFieldMetadataIds?.length - 1];

const lastTargetRelationFieldMetadata = await this.getFieldMetadata(
workspaceId,
lastTargetRelationFieldMetadataId,
);

const columnName =
targetMeasureFieldMetadata?.name ?? lastTargetRelationFieldMetadata?.name;
measureFieldMetadata?.name ?? lastTargetRelationFieldMetadata?.name;

const lastQueryRelation: QueryRelation | undefined =
targetQueryRelations[targetQueryRelations.length - 1];
Expand Down Expand Up @@ -376,7 +379,7 @@ export class ChartService {
lastGroupByFieldMetadata) ||
undefined;

return {
const chartQuery: ChartQuery = {
sourceObjectMetadataId: sourceObjectMetadata.id,
target: {
relationFieldMetadataIds: targetMeasureFieldMetadata
Expand All @@ -390,12 +393,13 @@ export class ChartService {
? chart.groupBy.slice(0, -1)
: [],
measureFieldMetadataId: groupByMeasureFieldMetadata?.id,
measure: undefined as ChartMeasure | undefined,
// Think through groups implementation (for numeric values).
// excludeEmptyValues?: boolean;
measure: undefined,
groups: undefined,
includeNulls: undefined,
},
// Later: Filters should be included in the CHART_QUERY UI => better to also store them in CHART_QUERY JSON?
};

return chartQuery;
}

async run(workspaceId: string, chartId: string): Promise<ChartResult> {
Expand All @@ -412,17 +416,16 @@ export class ChartService {
},
);

const targetMeasureFieldMetadata =
(await this.getFieldMetadata(
workspaceId,
chartQuery.target.measureFieldMetadataId,
)) ?? undefined;
const targetMeasureFieldMetadata = await this.getFieldMetadata(
workspaceId,
chartQuery.target?.measureFieldMetadataId,
);

const sourceQueryRelation = await this.getSourceQueryRelation(
dataSourceSchemaName,
workspaceId,
sourceObjectMetadata,
chartQuery.target.relationFieldMetadataIds,
chartQuery.target?.relationFieldMetadataIds,
targetMeasureFieldMetadata,
);

Expand All @@ -431,7 +434,7 @@ export class ChartService {
workspaceId,
sourceObjectMetadata,
'target',
chartQuery.target.relationFieldMetadataIds,
chartQuery.target?.relationFieldMetadataIds,
targetMeasureFieldMetadata,
);

Expand All @@ -446,39 +449,51 @@ export class ChartService {
workspaceId,
targetQueryRelations,
sourceQueryRelation.tableName,
chartQuery.target.relationFieldMetadataIds,
chartQuery.target?.relationFieldMetadataIds,
targetMeasureFieldMetadata,
);

/*const groupByJoinOperations = await this.getJoinOperations(
const groupByMeasureFieldMetadata = await this.getFieldMetadata(
workspaceId,
chart.sourceObjectNameSingular,
chartQuery.groupBy?.measureFieldMetadataId,
);

const groupByJoinOperations = await this.getQueryRelations(
dataSourceSchemaName,
workspaceId,
sourceObjectMetadata,
'group_by',
chart.groupBy,
chartQuery.groupBy?.relationFieldMetadataIds,
groupByMeasureFieldMetadata,
);

const groupByJoinClauses = this.getJoinClauses(
dataSourceSchema,
groupByJoinOperations,
const groupByQueryRelations = await this.getQueryRelations(
dataSourceSchemaName,
workspaceId,
sourceObjectMetadata,
'target',
chartQuery.target?.relationFieldMetadataIds,
targetMeasureFieldMetadata,
);

const [groupByTableAlias, groupByColumnName] =
await this.getTableAliasAndColumn(
workspaceId,
groupByJoinOperations,
sourceTableName,
chart.groupBy,
);
const groupByJoinClauses = this.getJoinClauses(
dataSourceSchemaName,
groupByQueryRelations,
);

const groupBySelectColumn =
chart.groupBy && chart.groupBy.length > 0
? `"${groupByTableAlias}"."${groupByColumnName}"`
: undefined;
const groupByQualifiedColumn = await this.getQualifiedColumn(
workspaceId,
groupByQueryRelations,
sourceQueryRelation.tableName,
chartQuery.groupBy?.relationFieldMetadataIds,
groupByMeasureFieldMetadata,
);

const groupByClause =
chart.groupBy && chart.groupBy.length > 0
? `GROUP BY "${groupByTableAlias}"."${groupByColumnName}"`
: '';*/
chartQuery.groupBy?.measureFieldMetadataId ||
chartQuery.groupBy?.relationFieldMetadataIds
? `GROUP BY "${groupByQualifiedColumn}"`
: '';

const allQueryRelations = [
sourceQueryRelation,
Expand All @@ -492,8 +507,12 @@ export class ChartService {

console.log('commonTableExpressions', commonTableExpressions);

if (chartQuery.target?.measure === undefined) {
throw new Error('Measure is currently required');
}

const measureSelectColumn = this.getMeasureSelectColumn(
chartQuery.target.measure,
chartQuery.target?.measure,
targetQualifiedColumn,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ChartQueryMeasure } from 'src/modules/charts/standard-objects/chart.workspace-entity';

export interface ChartQuery {
sourceObjectMetadataId?: string;
target?: {
relationFieldMetadataIds?: string[];
measureFieldMetadataId?: string;
measure?: ChartQueryMeasure;
};
groupBy?: {
relationFieldMetadataIds?: string[];
measureFieldMetadataId?: string;
measure?: ChartQueryMeasure;
groups?: { upperLimit: number; lowerLimit: number }[];
includeNulls?: boolean;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is
import { CHART_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';

export enum ChartMeasure {
export enum ChartQueryMeasure {
AVERAGE = 'AVERAGE',
SUM = 'SUM',
MIN = 'MIN',
Expand Down Expand Up @@ -58,39 +58,39 @@ export class ChartWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconRulerMeasure',
options: [
{
value: ChartMeasure.AVERAGE,
value: ChartQueryMeasure.AVERAGE,
label: 'Average',
position: 0,
color: 'blue',
},
{
value: ChartMeasure.SUM,
value: ChartQueryMeasure.SUM,
label: 'Sum',
position: 1,
color: 'green',
},
{
value: ChartMeasure.MIN,
value: ChartQueryMeasure.MIN,
label: 'Minimum',
position: 2,
color: 'orange',
},
{
value: ChartMeasure.MAX,
value: ChartQueryMeasure.MAX,
label: 'Maximum',
position: 3,
color: 'red',
},
{
value: ChartMeasure.COUNT,
value: ChartQueryMeasure.COUNT,
label: 'Count',
position: 4,
color: 'purple',
},
],
defaultValue: `'${ChartMeasure.COUNT}'`,
defaultValue: `'${ChartQueryMeasure.COUNT}'`,
})
measure: ChartMeasure;
measure: ChartQueryMeasure;

@WorkspaceField({
standardId: CHART_STANDARD_FIELD_IDS.sourceObjectNameSingular,
Expand Down

0 comments on commit f8e591f

Please sign in to comment.