From 4bccf07bb34fed96b97b7054bb59722bd5653677 Mon Sep 17 00:00:00 2001 From: An Phi Date: Thu, 23 Jan 2025 09:29:31 -0500 Subject: [PATCH] datacube: improve roundtrip (#3820) * datacube: allow choosing vX_X_X protocol version for execution * datacube: support type-checking for leaf-level extend() * datacube: roundtrip test for extend(), sort(), limit(), select() * datacube: verify variable when processing lambdas * datacube: roundtrip processing and test for aggregation * datacube: support sorting pivot result columns * datacube: roundtrip tests for pivot and duplicate columns check * datacube: more validations for groupBy and pivot * datacube: roundtrip check for configuration --- .changeset/large-lemons-sit.md | 7 + .changeset/perfect-planets-knock.md | 7 + .../stores/LegendDataCubeDataCubeEngine.ts | 90 +- .../src/stores/LegendREPLDataCubeEngine.ts | 7 + .../docs/column-configuration.kind.md | 2 + .../src/__lib__/DataCubeSetting.ts | 1 + .../DataCubeEditorColumnPropertiesPanel.tsx | 57 +- .../editor/DataCubeEditorColumnSelector.tsx | 76 +- .../editor/DataCubeEditorColumnsPanel.tsx | 9 +- .../DataCubeEditorHorizontalPivotsPanel.tsx | 17 +- .../view/editor/DataCubeEditorSortsPanel.tsx | 92 +- .../view/filter/DataCubeFilterEditor.tsx | 5 +- .../core/DataCubeConfigurationBuilder.ts | 85 +- .../src/stores/core/DataCubeEngine.tsx | 12 +- .../src/stores/core/DataCubeQueryBuilder.ts | 73 +- .../stores/core/DataCubeQueryBuilderUtils.ts | 115 +- .../src/stores/core/DataCubeQueryEngine.ts | 180 +- .../src/stores/core/DataCubeQuerySnapshot.ts | 13 + .../core/DataCubeQuerySnapshotBuilder.ts | 536 ++++-- .../core/DataCubeQuerySnapshotBuilderUtils.ts | 754 +++++++-- .../DataCubeQueryRoundtrip.data-cube-test.ts | 1505 +++++++++++++++-- .../core/__tests__/DataCubeTestUtils.ts | 85 +- .../DataCubeQueryAggregateOperation.ts | 55 +- ...taCubeQueryAggregateOperation__Average.tsx | 30 +- ...DataCubeQueryAggregateOperation__Count.tsx | 30 +- ...DataCubeQueryAggregateOperation__First.tsx | 30 +- ...beQueryAggregateOperation__JoinStrings.tsx | 67 +- .../DataCubeQueryAggregateOperation__Last.tsx | 30 +- .../DataCubeQueryAggregateOperation__Max.tsx | 30 +- .../DataCubeQueryAggregateOperation__Min.tsx | 30 +- ...ryAggregateOperation__StdDevPopulation.tsx | 35 +- ...eQueryAggregateOperation__StdDevSample.tsx | 34 +- .../DataCubeQueryAggregateOperation__Sum.tsx | 30 +- ...beQueryAggregateOperation__UniqueValue.tsx | 30 +- ...AggregateOperation__VariancePopulation.tsx | 30 +- ...ueryAggregateOperation__VarianceSample.tsx | 30 +- .../filter/DataCubeQueryFilterOperation.ts | 53 +- .../DataCubeQueryFilterOperation__Contain.tsx | 21 +- ...ilterOperation__ContainCaseInsensitive.tsx | 23 +- .../DataCubeQueryFilterOperation__EndWith.tsx | 19 +- ...ilterOperation__EndWithCaseInsensitive.tsx | 23 +- .../DataCubeQueryFilterOperation__Equal.tsx | 13 +- ...yFilterOperation__EqualCaseInsensitive.tsx | 23 +- ...rOperation__EqualCaseInsensitiveColumn.tsx | 14 +- ...aCubeQueryFilterOperation__EqualColumn.tsx | 12 +- ...aCubeQueryFilterOperation__GreaterThan.tsx | 15 +- ...ueryFilterOperation__GreaterThanColumn.tsx | 14 +- ...eryFilterOperation__GreaterThanOrEqual.tsx | 15 +- ...terOperation__GreaterThanOrEqualColumn.tsx | 14 +- ...ataCubeQueryFilterOperation__IsNotNull.tsx | 14 +- .../DataCubeQueryFilterOperation__IsNull.tsx | 8 +- ...DataCubeQueryFilterOperation__LessThan.tsx | 15 +- ...beQueryFilterOperation__LessThanColumn.tsx | 14 +- ...eQueryFilterOperation__LessThanOrEqual.tsx | 15 +- ...FilterOperation__LessThanOrEqualColumn.tsx | 14 +- ...taCubeQueryFilterOperation__NotContain.tsx | 27 +- ...taCubeQueryFilterOperation__NotEndWith.tsx | 27 +- ...DataCubeQueryFilterOperation__NotEqual.tsx | 24 +- ...lterOperation__NotEqualCaseInsensitive.tsx | 31 +- ...eration__NotEqualCaseInsensitiveColumn.tsx | 26 +- ...beQueryFilterOperation__NotEqualColumn.tsx | 26 +- ...CubeQueryFilterOperation__NotStartWith.tsx | 27 +- ...ataCubeQueryFilterOperation__StartWith.tsx | 19 +- ...terOperation__StartWithCaseInsensitive.tsx | 23 +- .../src/stores/core/model/DataCubeColumn.ts | 2 +- .../core/model/DataCubeConfiguration.ts | 8 +- .../services/DataCubeQuerySnapshotService.ts | 3 +- .../services/DataCubeSettingService.tsx | 8 + .../src/stores/view/DataCubeViewState.ts | 5 +- ...ataCubeEditorColumnPropertiesPanelState.ts | 5 +- .../DataCubeEditorColumnSelectorState.ts | 37 +- .../editor/DataCubeEditorColumnsPanelState.ts | 9 +- ...ataCubeEditorHorizontalPivotsPanelState.ts | 43 +- .../DataCubeEditorMutableConfiguration.ts | 24 +- .../editor/DataCubeEditorSortsPanelState.ts | 35 +- .../view/editor/DataCubeEditorState.tsx | 5 +- .../DataCubeEditorVerticalPivotsPanelState.ts | 13 +- .../view/extend/DataCubeColumnEditorState.tsx | 5 +- .../extend/DataCubeExtendManagerState.tsx | 27 +- .../view/filter/DataCubeFilterEditorState.tsx | 6 +- .../view/grid/DataCubeGridClientEngine.ts | 41 +- .../grid/DataCubeGridConfigurationBuilder.tsx | 79 +- .../view/grid/DataCubeGridControllerState.ts | 69 +- .../view/grid/DataCubeGridMenuBuilder.tsx | 40 +- .../view/grid/DataCubeGridQueryBuilder.ts | 2 +- .../grid/DataCubeGridQuerySnapshotBuilder.ts | 2 +- packages/legend-data-cube/style/index.scss | 18 + .../__test-utils__/EngineTestSupport.ts | 37 +- .../data-cube/QueryBuilderDataCubeEngine.ts | 172 +- 89 files changed, 3966 insertions(+), 1517 deletions(-) create mode 100644 .changeset/large-lemons-sit.md create mode 100644 .changeset/perfect-planets-knock.md diff --git a/.changeset/large-lemons-sit.md b/.changeset/large-lemons-sit.md new file mode 100644 index 0000000000..894e52e290 --- /dev/null +++ b/.changeset/large-lemons-sit.md @@ -0,0 +1,7 @@ +--- +'@finos/legend-application-data-cube': patch +'@finos/legend-application-repl': patch +'@finos/legend-query-builder': patch +'@finos/legend-data-cube': patch +'@finos/legend-graph': patch +--- diff --git a/.changeset/perfect-planets-knock.md b/.changeset/perfect-planets-knock.md new file mode 100644 index 0000000000..894e52e290 --- /dev/null +++ b/.changeset/perfect-planets-knock.md @@ -0,0 +1,7 @@ +--- +'@finos/legend-application-data-cube': patch +'@finos/legend-application-repl': patch +'@finos/legend-query-builder': patch +'@finos/legend-data-cube': patch +'@finos/legend-graph': patch +--- diff --git a/packages/legend-application-data-cube/src/stores/LegendDataCubeDataCubeEngine.ts b/packages/legend-application-data-cube/src/stores/LegendDataCubeDataCubeEngine.ts index b915673923..c717a1c9d5 100644 --- a/packages/legend-application-data-cube/src/stores/LegendDataCubeDataCubeEngine.ts +++ b/packages/legend-application-data-cube/src/stores/LegendDataCubeDataCubeEngine.ts @@ -60,6 +60,7 @@ import { _serializeValueSpecification, _deserializeValueSpecification, _defaultPrimitiveTypeValue, + type DataCubeExecutionOptions, } from '@finos/legend-data-cube'; import { isNonNullable, @@ -224,6 +225,46 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine { } } + override async parseValueSpecification( + code: string, + returnSourceInformation?: boolean | undefined, + ) { + try { + return _deserializeValueSpecification( + await this._engineServerClient.grammarToJSON_valueSpecification( + code, + '', + undefined, + undefined, + returnSourceInformation, + ), + ); + } catch (error) { + assertErrorThrown(error); + if ( + error instanceof NetworkClientError && + error.response.status === HttpStatus.BAD_REQUEST + ) { + throw V1_buildParserError( + V1_ParserError.serialization.fromJson( + error.payload as PlainObject, + ), + ); + } + throw error; + } + } + + override async getValueSpecificationCode( + value: V1_ValueSpecification, + pretty?: boolean | undefined, + ) { + return this._graphManager.valueSpecificationToPureCode( + _serializeValueSpecification(value), + pretty, + ); + } + // TODO: we could optimize this by synthesizing the base query from the source columns // instead of having to send the entire graph model override async getQueryTypeahead( @@ -256,19 +297,14 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine { ); } - override async parseValueSpecification( - code: string, - returnSourceInformation?: boolean | undefined, + override async getQueryRelationReturnType( + query: V1_Lambda, + source: DataCubeSource, ) { try { - return _deserializeValueSpecification( - await this._engineServerClient.grammarToJSON_valueSpecification( - code, - '', - undefined, - undefined, - returnSourceInformation, - ), + return await this._getQueryRelationType( + _serializeValueSpecification(query), + source, ); } catch (error) { assertErrorThrown(error); @@ -276,26 +312,17 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine { error instanceof NetworkClientError && error.response.status === HttpStatus.BAD_REQUEST ) { - throw V1_buildParserError( - V1_ParserError.serialization.fromJson( - error.payload as PlainObject, + const engineError = V1_buildEngineError( + V1_EngineError.serialization.fromJson( + error.payload as PlainObject, ), ); + throw engineError; } throw error; } } - override async getValueSpecificationCode( - value: V1_ValueSpecification, - pretty?: boolean | undefined, - ) { - return this._graphManager.valueSpecificationToPureCode( - _serializeValueSpecification(value), - pretty, - ); - } - // TODO: we could optimize this by synthesizing the base query from the source columns // instead of having to send the entire graph model override async getQueryCodeRelationReturnType( @@ -335,17 +362,22 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine { } } - override async executeQuery(query: V1_Lambda, source: DataCubeSource) { + override async executeQuery( + query: V1_Lambda, + source: DataCubeSource, + options?: DataCubeExecutionOptions | undefined, + ) { const queryCodePromise = this.getValueSpecificationCode(query); let result: ExecutionResult; if (source instanceof AdhocQueryDataCubeSource) { - result = await this._runQuery(query, source.model); + result = await this._runQuery(query, source.model, undefined, options); } else if (source instanceof LegendQueryDataCubeSource) { query.parameters = source.lambda.parameters; result = await this._runQuery( query, source.model, source.parameterValues, + options, ); } else { throw new UnsupportedOperationError( @@ -429,15 +461,17 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine { query: V1_Lambda, model: PlainObject, parameterValues?: V1_ParameterValue[] | undefined, + options?: DataCubeExecutionOptions | undefined, ): Promise { return V1_buildExecutionResult( V1_deserializeExecutionResult( (await this._engineServerClient.runQuery({ clientVersion: + options?.clientVersion ?? // eslint-disable-next-line no-process-env - process.env.NODE_ENV === 'development' + (process.env.NODE_ENV === 'development' ? PureClientVersion.VX_X_X - : undefined, + : undefined), function: _serializeValueSpecification(query), model, context: serialize( diff --git a/packages/legend-application-repl/src/stores/LegendREPLDataCubeEngine.ts b/packages/legend-application-repl/src/stores/LegendREPLDataCubeEngine.ts index df2bb3bd46..e014f050da 100644 --- a/packages/legend-application-repl/src/stores/LegendREPLDataCubeEngine.ts +++ b/packages/legend-application-repl/src/stores/LegendREPLDataCubeEngine.ts @@ -144,6 +144,13 @@ export class LegendREPLDataCubeEngine extends DataCubeEngine { }); } + override async getQueryRelationReturnType( + query: V1_Lambda, + source: DataCubeSource, + ) { + return this._getQueryRelationType(query); + } + override async getQueryCodeRelationReturnType( code: string, baseQuery: V1_ValueSpecification, diff --git a/packages/legend-data-cube/docs/column-configuration.kind.md b/packages/legend-data-cube/docs/column-configuration.kind.md index e0bf3f633a..6f852bc056 100644 --- a/packages/legend-data-cube/docs/column-configuration.kind.md +++ b/packages/legend-data-cube/docs/column-configuration.kind.md @@ -5,6 +5,8 @@ id: 'data-cube.column-configuration.kind' A column represents either a `dimension` or a `measure`. +> Column kind cannot be changed while the column is used in pivot. + # Dimension Descriptions that help group and filter data. Typically associated with string columns where each value represents a category. diff --git a/packages/legend-data-cube/src/__lib__/DataCubeSetting.ts b/packages/legend-data-cube/src/__lib__/DataCubeSetting.ts index ff2458f18f..a595c98c29 100644 --- a/packages/legend-data-cube/src/__lib__/DataCubeSetting.ts +++ b/packages/legend-data-cube/src/__lib__/DataCubeSetting.ts @@ -16,6 +16,7 @@ export enum DataCubeSettingKey { DEBUGGER__ENABLE_DEBUG_MODE = 'dataCube.debugger.enableDebugMode', + DEBUGGER__USE_DEV_CLIENT_PROTOCOL_VERSION = 'dataCube.debugger.useDevClientProtocolVersion', DEBUGGER__ACTION__RELOAD = 'dataCube.debugger.action.reload', GRID_CLIENT__ROW_BUFFER = 'dataCube.grid.rowBuffer', GRID_CLIENT__PURGE_CLOSED_ROW_NODES = 'dataCube.grid.purgeClosedRowNodes', diff --git a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnPropertiesPanel.tsx b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnPropertiesPanel.tsx index 2c934c30ec..98a69f641e 100644 --- a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnPropertiesPanel.tsx +++ b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnPropertiesPanel.tsx @@ -47,7 +47,10 @@ import { } from '../../../stores/core/DataCubeQueryEngine.js'; import { DataCubeDocumentationKey } from '../../../__lib__/DataCubeDocumentation.js'; import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.js'; -import { _sortByColName } from '../../../stores/core/model/DataCubeColumn.js'; +import { + _findCol, + _sortByColName, +} from '../../../stores/core/model/DataCubeColumn.js'; export const DataCubeEditorColumnPropertiesPanel = observer( (props: { view: DataCubeViewState }) => { @@ -150,17 +153,16 @@ export const DataCubeEditorColumnPropertiesPanel = observer( {selectedColumn.dataType} {Boolean( - editor.leafExtendColumns.find( - (col) => col.name === selectedColumn.name, - ), + _findCol(editor.leafExtendColumns, selectedColumn.name), ) && (
{`Extended (Leaf Level)`}
)} {Boolean( - editor.groupExtendColumns.find( - (col) => col.name === selectedColumn.name, + _findCol( + editor.groupExtendColumns, + selectedColumn.name, ), ) && (
@@ -188,18 +190,14 @@ export const DataCubeEditorColumnPropertiesPanel = observer( {column.dataType}
{Boolean( - editor.leafExtendColumns.find( - (col) => col.name === column.name, - ), + _findCol(editor.leafExtendColumns, column.name), ) && (
{`Extended (Leaf Level)`}
)} {Boolean( - editor.groupExtendColumns.find( - (col) => col.name === column.name, - ), + _findCol(editor.groupExtendColumns, column.name), ) && (
{`Extended (Group Level)`} @@ -227,13 +225,29 @@ export const DataCubeEditorColumnPropertiesPanel = observer( open={kindDropdownPropsOpen} // disallow changing the column kind if the column is being used as pivot column disabled={Boolean( - editor.verticalPivots.selector.selectedColumns.find( - (col) => col.name === selectedColumn.name, + _findCol( + editor.verticalPivots.selector.selectedColumns, + selectedColumn.name, ) ?? - editor.horizontalPivots.selector.selectedColumns.find( - (col) => col.name === selectedColumn.name, + _findCol( + editor.horizontalPivots.selector.selectedColumns, + selectedColumn.name, ), )} + title={ + Boolean( + _findCol( + editor.verticalPivots.selector.selectedColumns, + selectedColumn.name, + ) ?? + _findCol( + editor.horizontalPivots.selector.selectedColumns, + selectedColumn.name, + ), + ) + ? 'Column kind cannot be changed while the column is used in pivot' + : undefined + } > {selectedColumn.kind} @@ -314,7 +328,14 @@ export const DataCubeEditorColumnPropertiesPanel = observer( { - selectedColumn.setAggregateOperation(op); + if (op !== selectedColumn.aggregateOperation) { + selectedColumn.setAggregateOperation(op); + selectedColumn.setAggregationParameters( + op.generateDefaultParameterValues( + selectedColumn, + ), + ); + } closeAggregationOperationDropdown(); }} autoFocus={op === selectedColumn.aggregateOperation} @@ -324,6 +345,8 @@ export const DataCubeEditorColumnPropertiesPanel = observer( ))} + {/* TODO: Support editing aggregation parameter values */} + ; + column: DataCubeEditorSortColumnState; + }) => { + const { column } = props; + const [ + openDirectionDropdown, + closeDirectionDropdown, + directionDropdownProps, + directionDropdownPropsOpen, + ] = useDropdownMenu(); + + return ( +
+ {!directionDropdownPropsOpen && ( +
+ {column.direction} +
+ )} + {directionDropdownPropsOpen && ( +
+
{column.direction}
+
+ +
+
+ )} + + + {[ + DataCubeQuerySortDirection.ASCENDING, + DataCubeQuerySortDirection.DESCENDING, + ].map((direction) => ( + { + column.setDirection(direction); + closeDirectionDropdown(); + }} + autoFocus={column.direction === direction} + > + {direction} + + ))} + +
+ ); + }, +); diff --git a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnsPanel.tsx b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnsPanel.tsx index 76c3b86fd0..956e5bf14e 100644 --- a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnsPanel.tsx +++ b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorColumnsPanel.tsx @@ -22,6 +22,7 @@ import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.j import { FormCheckbox } from '../../core/DataCubeFormUtils.js'; import type { DataCubeEditorColumnSelectorColumnState } from '../../../stores/view/editor/DataCubeEditorColumnSelectorState.js'; import type { DataCubeEditorState } from '../../../stores/view/editor/DataCubeEditorState.js'; +import { _findCol } from '../../../stores/core/model/DataCubeColumn.js'; const ColumnSelectorLabel = observer( (props: { @@ -45,16 +46,12 @@ const ColumnSelectorLabel = observer( > {column.name}
- {Boolean( - editor.leafExtendColumns.find((col) => col.name === column.name), - ) && ( + {Boolean(_findCol(editor.leafExtendColumns, column.name)) && (
{`Extended (Leaf Level)`}
)} - {Boolean( - editor.groupExtendColumns.find((col) => col.name === column.name), - ) && ( + {Boolean(_findCol(editor.groupExtendColumns, column.name)) && (
{`Extended (Group Level)`}
diff --git a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorHorizontalPivotsPanel.tsx b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorHorizontalPivotsPanel.tsx index d19749700a..080e3e6c83 100644 --- a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorHorizontalPivotsPanel.tsx +++ b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorHorizontalPivotsPanel.tsx @@ -17,18 +17,11 @@ import { DataCubeIcon } from '@finos/legend-art'; import { observer } from 'mobx-react-lite'; import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.js'; -import { DataCubeEditorColumnSelector } from './DataCubeEditorColumnSelector.js'; +import { + DataCubeColumnSelectorSortDirectionDropdown, + DataCubeEditorColumnSelector, +} from './DataCubeEditorColumnSelector.js'; import { useEffect } from 'react'; -import { FormBadge_WIP } from '../../core/DataCubeFormUtils.js'; - -const PivotColumnSortDirectionDropdown = observer((props) => ( -
-
- Ascending - -
-
-)); export const DataCubeEditorHorizontalPivotsPanel = observer( (props: { view: DataCubeViewState }) => { @@ -51,7 +44,7 @@ export const DataCubeEditorHorizontalPivotsPanel = observer( ( - + )} /> diff --git a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorSortsPanel.tsx b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorSortsPanel.tsx index 0e422d24cc..6b84d0ba00 100644 --- a/packages/legend-data-cube/src/components/view/editor/DataCubeEditorSortsPanel.tsx +++ b/packages/legend-data-cube/src/components/view/editor/DataCubeEditorSortsPanel.tsx @@ -15,88 +15,18 @@ */ import { observer } from 'mobx-react-lite'; -import { DataCubeIcon, useDropdownMenu } from '@finos/legend-art'; -import { DataCubeEditorColumnSelector } from './DataCubeEditorColumnSelector.js'; -import type { DataCubeEditorColumnSelectorState } from '../../../stores/view/editor/DataCubeEditorColumnSelectorState.js'; -import type { DataCubeEditorSortColumnState } from '../../../stores/view/editor/DataCubeEditorSortsPanelState.js'; +import { DataCubeIcon } from '@finos/legend-art'; import { - DataCubeQuerySortDirection, - PIVOT_COLUMN_NAME_VALUE_SEPARATOR, -} from '../../../stores/core/DataCubeQueryEngine.js'; -import { - FormDropdownMenu, - FormDropdownMenuItem, -} from '../../core/DataCubeFormUtils.js'; + DataCubeColumnSelectorSortDirectionDropdown, + DataCubeEditorColumnSelector, +} from './DataCubeEditorColumnSelector.js'; +import type { + DataCubeEditorColumnSelectorState, + DataCubeEditorSortColumnState, +} from '../../../stores/view/editor/DataCubeEditorColumnSelectorState.js'; +import { PIVOT_COLUMN_NAME_VALUE_SEPARATOR } from '../../../stores/core/DataCubeQueryEngine.js'; import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.js'; -const SortDirectionDropdown = observer( - (props: { - selector: DataCubeEditorColumnSelectorState; - column: DataCubeEditorSortColumnState; - }) => { - const { column } = props; - const [ - openDirectionDropdown, - closeDirectionDropdown, - directionDropdownProps, - directionDropdownPropsOpen, - ] = useDropdownMenu(); - - return ( -
- {!directionDropdownPropsOpen && ( -
- {column.direction} -
- )} - {directionDropdownPropsOpen && ( -
-
{column.direction}
-
- -
-
- )} - - - {[ - DataCubeQuerySortDirection.ASCENDING, - DataCubeQuerySortDirection.DESCENDING, - ].map((direction) => ( - { - column.setDirection(direction); - closeDirectionDropdown(); - }} - autoFocus={column.direction === direction} - > - {direction} - - ))} - -
- ); - }, -); - const SortColumnLabel = observer( (props: { selector: DataCubeEditorColumnSelectorState; @@ -131,7 +61,9 @@ export const DataCubeEditorSortsPanel = observer( } - columnActionRenderer={(p) => } + columnActionRenderer={(p) => ( + + )} /> diff --git a/packages/legend-data-cube/src/components/view/filter/DataCubeFilterEditor.tsx b/packages/legend-data-cube/src/components/view/filter/DataCubeFilterEditor.tsx index bbb6e32c67..a71502586c 100644 --- a/packages/legend-data-cube/src/components/view/filter/DataCubeFilterEditor.tsx +++ b/packages/legend-data-cube/src/components/view/filter/DataCubeFilterEditor.tsx @@ -48,6 +48,7 @@ import { DATE_FORMAT, PRIMITIVE_TYPE } from '@finos/legend-graph'; import { formatDate, guaranteeIsNumber, parseISO } from '@finos/legend-shared'; import { evaluate } from 'mathjs'; import { useDataCube } from '../../DataCubeProvider.js'; +import { _findCol } from '../../../stores/core/model/DataCubeColumn.js'; const FILTER_TREE_OFFSET = 10; const FILTER_TREE_INDENTATION_SPACE = 36; @@ -286,9 +287,7 @@ const DataCubeEditorFilterConditionNodeColumnSelector = observer( >(function DataCubeEditorFilterConditionNodeColumnSelector(props, ref) { const { value, updateValue, view } = props; const editor = view.filter; - const matchingColumn = editor.columns.find( - (column) => column.name === value, - ); + const matchingColumn = _findCol(editor.columns, value); const [ openColumnDropdown, closeColumnDropdown, diff --git a/packages/legend-data-cube/src/stores/core/DataCubeConfigurationBuilder.ts b/packages/legend-data-cube/src/stores/core/DataCubeConfigurationBuilder.ts index b70d0b1926..3bf976a4ab 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeConfigurationBuilder.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeConfigurationBuilder.ts @@ -24,13 +24,17 @@ import { DataCubeColumnKind, DataCubeFontTextAlignment, } from './DataCubeQueryEngine.js'; -import type { DataCubeColumn } from './model/DataCubeColumn.js'; +import { _findCol, type DataCubeColumn } from './model/DataCubeColumn.js'; +import type { DataCubeQuerySnapshotProcessingContext } from './DataCubeQuerySnapshot.js'; +import { at } from '@finos/legend-shared'; -export function buildDefaultColumnConfiguration( +export function newColumnConfiguration( column: DataCubeColumn, + context?: DataCubeQuerySnapshotProcessingContext | undefined, ): DataCubeColumnConfiguration { const { name, type } = column; const config = new DataCubeColumnConfiguration(name, type); + switch (type) { case PRIMITIVE_TYPE.NUMBER: case PRIMITIVE_TYPE.INTEGER: @@ -53,15 +57,86 @@ export function buildDefaultColumnConfiguration( break; } } + + if (context) { + const { snapshot, groupByAggColumns, pivotAggColumns, pivotSortColumns } = + context; + const data = snapshot.data; + + // process column selection + config.isSelected = Boolean( + _findCol(data.groupExtendedColumns, name) ?? + _findCol(data.selectColumns, name), + ); + + if (data.groupBy ?? data.pivot) { + const groupByAggCol = _findCol(groupByAggColumns, name); + const pivotAggCol = _findCol(pivotAggColumns, name); + const aggCol = groupByAggCol ?? pivotAggCol; + + // process column kind + if (aggCol) { + config.kind = DataCubeColumnKind.MEASURE; + } else if ( + _findCol(data.pivot?.columns ?? [], name) ?? + _findCol(data.groupBy?.columns ?? [], name) + ) { + config.kind = DataCubeColumnKind.DIMENSION; + } + + // aggregation + if (aggCol) { + config.aggregateOperator = aggCol.operator; + config.aggregationParameters = aggCol.parameterValues; + } + + // exclude from pivot + if (data.pivot) { + config.excludedFromPivot = !pivotAggCol; + } + + // process pivot sort direction + if (data.pivot) { + const pivotSortCol = _findCol(pivotSortColumns, name); + if (pivotSortCol) { + config.pivotSortDirection = pivotSortCol.direction; + } + } + } + } + return config; } -export function buildDefaultConfiguration( - columns: DataCubeColumn[], +export function newConfiguration( + context: DataCubeQuerySnapshotProcessingContext, ): DataCubeConfiguration { + const { snapshot, groupBySortColumns } = context; + const data = snapshot.data; const configuration = new DataCubeConfiguration(); + + // NOTE: the order of the column configurations is determined by + // the order of columns specified in the select() expression. + // Unselected columns will follow the order they are defined (i.e. source + // columns followed by leaf-level extended columns) + const columns = [ + ...data.selectColumns, + ...data.groupExtendedColumns, + ...[...data.sourceColumns, ...data.leafExtendedColumns].filter( + (col) => !_findCol(data.selectColumns, col.name), + ), + ]; configuration.columns = columns.map((column) => - buildDefaultColumnConfiguration(column), + newColumnConfiguration(column, context), ); + + // process tree column sort direction + // + // since we have made sure all groupBy sort columns must be of the same direction + // we simply retrieve the direction from the one of the column provided + if (groupBySortColumns.length) { + configuration.treeColumnSortDirection = at(groupBySortColumns, 0).direction; + } + return configuration; } diff --git a/packages/legend-data-cube/src/stores/core/DataCubeEngine.tsx b/packages/legend-data-cube/src/stores/core/DataCubeEngine.tsx index 5ddc6bb06f..8bbe1443fc 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeEngine.tsx +++ b/packages/legend-data-cube/src/stores/core/DataCubeEngine.tsx @@ -21,8 +21,10 @@ import { type V1_AppliedFunction, PRIMITIVE_TYPE, } from '@finos/legend-graph'; -import { getFilterOperation } from './filter/DataCubeQueryFilterOperation.js'; -import { getAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js'; +import { + getFilterOperation, + getAggregateOperation, +} from './DataCubeQueryEngine.js'; import { DataCubeQueryAggregateOperation__Sum } from './aggregation/DataCubeQueryAggregateOperation__Sum.js'; import { DataCubeQueryAggregateOperation__Average } from './aggregation/DataCubeQueryAggregateOperation__Average.js'; import { DataCubeQueryAggregateOperation__Count } from './aggregation/DataCubeQueryAggregateOperation__Count.js'; @@ -88,6 +90,7 @@ export type DataCubeRelationType = { export type DataCubeExecutionOptions = { debug?: boolean | undefined; + clientVersion?: string | undefined; }; export type DataCubeExecutionResult = { @@ -211,6 +214,11 @@ export abstract class DataCubeEngine { source: DataCubeSource, ): Promise; + abstract getQueryRelationReturnType( + query: V1_Lambda, + source: DataCubeSource, + ): Promise; + abstract getQueryCodeRelationReturnType( code: string, baseQuery: V1_ValueSpecification, diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts index 3a439c6f66..c0d13cf4cf 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts @@ -23,7 +23,7 @@ import { PRIMITIVE_TYPE, type V1_AppliedFunction } from '@finos/legend-graph'; import { type DataCubeQuerySnapshot } from './DataCubeQuerySnapshot.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; +import { at } from '@finos/legend-shared'; import { DataCubeFunction, DataCubeQuerySortDirection, @@ -50,6 +50,7 @@ import { } from './DataCubeQueryBuilderUtils.js'; import type { DataCubeQueryAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js'; import type { DataCubeSource } from './model/DataCubeSource.js'; +import { _findCol } from './model/DataCubeColumn.js'; export function buildExecutableQuery( snapshot: DataCubeQuerySnapshot, @@ -93,24 +94,20 @@ export function buildExecutableQuery( // --------------------------------- LEAF-LEVEL EXTEND --------------------------------- if (data.leafExtendedColumns.length) { - const leafExtendedFuncs = data.leafExtendedColumns.map((col) => - _function(DataCubeFunction.EXTEND, [ - _cols([ - _colSpec( - col.name, - _deserializeLambda(col.mapFn), - col.reduceFn ? _deserializeLambda(col.reduceFn) : undefined, - ), + _process( + 'leafExtend', + data.leafExtendedColumns.map((col) => + _function(DataCubeFunction.EXTEND, [ + _cols([ + _colSpec( + col.name, + _deserializeLambda(col.mapFn), + col.reduceFn ? _deserializeLambda(col.reduceFn) : undefined, + ), + ]), ]), - ]), + ), ); - // instead of batching all the extend() functions, we sequence them to allow for - // different flavors of extend (e.g. with and without window), and reference the first - // one in the function map - _process('leafExtend', guaranteeNonNullable(leafExtendedFuncs[0])); - leafExtendedFuncs.slice(1).forEach((func) => { - sequence.push(func); - }); } // --------------------------------- FILTER --------------------------------- @@ -146,7 +143,13 @@ export function buildExecutableQuery( _function(DataCubeFunction.SORT, [ _collection( data.pivot.columns.map((col) => - _function(DataCubeFunction.ASCENDING, [_col(col.name)]), + _function( + _findCol(configuration.columns, col.name)?.pivotSortDirection === + DataCubeQuerySortDirection.DESCENDING + ? DataCubeFunction.DESCENDING + : DataCubeFunction.ASCENDING, + [_col(col.name)], + ), ), ), ]), @@ -217,24 +220,20 @@ export function buildExecutableQuery( // --------------------------------- GROUP-LEVEL EXTEND --------------------------------- if (data.groupExtendedColumns.length) { - const groupExtendedFuncs = data.groupExtendedColumns.map((col) => - _function(DataCubeFunction.EXTEND, [ - _cols([ - _colSpec( - col.name, - _deserializeLambda(col.mapFn), - col.reduceFn ? _deserializeLambda(col.reduceFn) : undefined, - ), + _process( + 'groupExtend', + data.groupExtendedColumns.map((col) => + _function(DataCubeFunction.EXTEND, [ + _cols([ + _colSpec( + col.name, + _deserializeLambda(col.mapFn), + col.reduceFn ? _deserializeLambda(col.reduceFn) : undefined, + ), + ]), ]), - ]), + ), ); - // instead of batching all the extend() functions, we sequence them to allow for - // different flavors of extend (e.g. with and without window), and reference the first - // one in the function map - _process('groupExtend', guaranteeNonNullable(groupExtendedFuncs[0])); - groupExtendedFuncs.slice(1).forEach((func) => { - sequence.push(func); - }); } // --------------------------------- SORT --------------------------------- @@ -299,9 +298,9 @@ export function buildExecutableQuery( return source.query; } for (let i = 0; i < sequence.length; i++) { - guaranteeNonNullable(sequence[i]).parameters.unshift( - i === 0 ? source.query : guaranteeNonNullable(sequence[i - 1]), + at(sequence, i).parameters.unshift( + i === 0 ? source.query : at(sequence, i - 1), ); } - return guaranteeNonNullable(sequence[sequence.length - 1]); + return at(sequence, sequence.length - 1); } diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilderUtils.ts b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilderUtils.ts index f9a0b70c9e..a902a5e7c0 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilderUtils.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilderUtils.ts @@ -60,7 +60,7 @@ import { type DataCubeQuerySnapshotFilter, type DataCubeQuerySnapshot, } from './DataCubeQuerySnapshot.js'; -import { type DataCubeColumn } from './model/DataCubeColumn.js'; +import { _findCol, type DataCubeColumn } from './model/DataCubeColumn.js'; import { guaranteeNonNullable, guaranteeIsString, @@ -95,9 +95,24 @@ export function _functionCompositionProcessor( sequence: V1_AppliedFunction[], funcMap: DataCubeQueryFunctionMap, ) { - return (key: keyof DataCubeQueryFunctionMap, func: V1_AppliedFunction) => { - sequence.push(func); - funcMap[key] = func; + return ( + key: keyof DataCubeQueryFunctionMap, + data: V1_AppliedFunction | V1_AppliedFunction[], + ) => { + switch (key) { + case 'leafExtend': + case 'groupExtend': { + if (Array.isArray(data)) { + data.forEach((func) => sequence.push(func)); + funcMap[key] = data; + } + break; + } + default: { + funcMap[key] = data as V1_AppliedFunction; + sequence.push(data as V1_AppliedFunction); + } + } }; } @@ -106,9 +121,13 @@ export function _functionCompositionUnProcessor( funcMap: DataCubeQueryFunctionMap, ) { return (key: keyof DataCubeQueryFunctionMap) => { - const func = funcMap[key]; - if (func) { - sequence.splice(sequence.indexOf(func), 1); + const data = funcMap[key]; + if (data) { + if (Array.isArray(data)) { + data.forEach((func) => sequence.splice(sequence.indexOf(func), 1)); + } else { + sequence.splice(sequence.indexOf(data), 1); + } funcMap[key] = undefined; } }; @@ -135,16 +154,17 @@ export function _deserializeFunction(json: PlainObject) { ); } -export function _var(name?: string | undefined) { +export function _var() { const variable = new V1_Variable(); - variable.name = name ?? DEFAULT_LAMBDA_VARIABLE_NAME; + // NOTE: we simplify processing logic by forcing all variable names to default value, i.e. x + variable.name = DEFAULT_LAMBDA_VARIABLE_NAME; return variable; } -export function _property(name: string, variable?: V1_Variable | undefined) { +export function _property(name: string) { const property = new V1_AppliedProperty(); property.property = name; - property.parameters.push(variable ?? _var()); + property.parameters.push(_var()); return property; } @@ -285,10 +305,7 @@ export function _colSpec( return colSpec; } -export function _value( - value: DataCubeOperationValue, - variable?: V1_Variable | undefined, -) { +export function _value(value: DataCubeOperationValue) { switch (value.type) { case PRIMITIVE_TYPE.STRING: case PRIMITIVE_TYPE.BOOLEAN: @@ -308,7 +325,7 @@ export function _value( return _primitiveValue(value.type, value.value); } case DataCubeOperationAdvancedValueType.COLUMN: - return _property(guaranteeIsString(value.value), variable); + return _property(guaranteeIsString(value.value)); default: throw new UnsupportedOperationError( `Can't build value instance for unsupported type '${value.type}'`, @@ -345,6 +362,13 @@ export function _selectFunction(columns: DataCubeColumn[]) { ]); } +export function _synthesizeMinimalSourceQuery(columns: DataCubeColumn[]) { + return _function(DataCubeFunction.CAST, [ + _primitiveValue(PRIMITIVE_TYPE.STRING, ''), + _castCols(columns), + ]); +} + export function _extendRootAggregation(columnName: string) { return _function(DataCubeFunction.EXTEND, [ _col( @@ -367,7 +391,7 @@ export function _extendRootAggregation(columnName: string) { const INTERNAL__FILLER_COUNT_AGG_COLUMN_NAME = 'INTERNAL__filler_count_agg_column'; // if no aggregates are specified, add a dummy count() aggregate to satisfy compiler -function fixEmptyAggCols(aggCols: V1_ColSpec[]) { +function _fixEmptyAggCols(aggCols: V1_ColSpec[]) { const variable = _var(); return aggCols.length ? aggCols @@ -380,12 +404,24 @@ function fixEmptyAggCols(aggCols: V1_ColSpec[]) { ]; } -export function _aggCol_basic(column: DataCubeColumn, func: string) { +export function _aggCol_base( + column: DataCubeColumn, + func: string, + paramterValues?: DataCubeOperationValue[] | undefined, +) { const variable = _var(); return _colSpec( column.name, - _lambda([variable], [_property(column.name, variable)]), - _lambda([variable], [_function(_functionName(func), [variable])]), + _lambda([variable], [_property(column.name)]), + _lambda( + [variable], + [ + _function(_functionName(func), [ + variable, + ...(paramterValues ?? []).map((value) => _value(value)), + ]), + ], + ), ); } @@ -401,18 +437,16 @@ export function _pivotAggCols( // unlike groupBy, pivot aggreation on dimension columns (e.g. unique values aggregator) // are not helpful and therefore excluded column.kind === DataCubeColumnKind.MEASURE && - !pivotColumns.find((col) => col.name === column.name) && + !_findCol(pivotColumns, column.name) && !column.excludedFromPivot && - !snapshot.data.groupExtendedColumns.find( - (col) => col.name === column.name, - ), + !_findCol(snapshot.data.groupExtendedColumns, column.name), ); - return fixEmptyAggCols( + return _fixEmptyAggCols( aggColumns.map((agg) => { const operation = aggregateOperations.find( (op) => op.operator === agg.aggregateOperator, ); - const aggCol = operation?.buildAggregateColumn(agg); + const aggCol = operation?.buildAggregateColumnExpression(agg); if (!aggCol) { throw new UnsupportedOperationError( `Can't build aggregate column for unsupported operator '${agg.aggregateOperator}'`, @@ -452,17 +486,15 @@ export function _groupByAggCols( const aggColumns = configuration.columns.filter( (column) => column.isSelected && - !groupByColumns.find((col) => col.name === column.name) && - !snapshot.data.groupExtendedColumns.find( - (col) => col.name === column.name, - ), + !_findCol(groupByColumns, column.name) && + !_findCol(snapshot.data.groupExtendedColumns, column.name), ); - return fixEmptyAggCols( + return _fixEmptyAggCols( aggColumns.map((agg) => { const operation = aggregateOperations.find( (op) => op.operator === agg.aggregateOperator, ); - const aggCol = operation?.buildAggregateColumn(agg); + const aggCol = operation?.buildAggregateColumnExpression(agg); if (!aggCol) { throw new UnsupportedOperationError( `Can't build aggregate column for unsupported operator '${agg.aggregateOperator}'`, @@ -479,15 +511,16 @@ export function _groupByAggCols( const pivotGroupByColumns = pivot.castColumns.filter( (col) => !isPivotResultColumnName(col.name), ); - return fixEmptyAggCols([ + return _fixEmptyAggCols([ // for pivot result columns, resolve the base aggregate column to get aggregate configuration ...pivotResultColumns .map((column) => { const baseAggColName = getPivotResultColumnBaseColumnName(column.name); return { ...column, - matchingColumnConfiguration: configuration.columns.find( - (col) => col.name === baseAggColName, + matchingColumnConfiguration: _findCol( + configuration.columns, + baseAggColName, ), }; }) @@ -503,7 +536,8 @@ export function _groupByAggCols( const operation = aggregateOperations.find( (op) => op.operator === columnConfiguration.aggregateOperator, ); - const aggCol = operation?.buildAggregateColumn(columnConfiguration); + const aggCol = + operation?.buildAggregateColumnExpression(columnConfiguration); if (!aggCol) { throw new UnsupportedOperationError( `Can't build aggregate column for unsupported operator '${columnConfiguration.aggregateOperator}'`, @@ -514,17 +548,16 @@ export function _groupByAggCols( // these are the columns which are available for groupBy but not selected for groupBy // operation, they would be aggregated as well ...pivotGroupByColumns - .filter( - (column) => !groupByColumns.find((col) => col.name === column.name), - ) + .filter((column) => !_findCol(groupByColumns, column.name)) .map((column) => { const columnConfiguration = guaranteeNonNullable( - configuration.columns.find((col) => col.name === column.name), + _findCol(configuration.columns, column.name), ); const operation = aggregateOperations.find( (op) => op.operator === columnConfiguration.aggregateOperator, ); - const aggCol = operation?.buildAggregateColumn(columnConfiguration); + const aggCol = + operation?.buildAggregateColumnExpression(columnConfiguration); if (!aggCol) { throw new UnsupportedOperationError( `Can't build aggregate column for unsupported operator '${columnConfiguration.aggregateOperator}'`, diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQueryEngine.ts b/packages/legend-data-cube/src/stores/core/DataCubeQueryEngine.ts index 2dd97f1169..87ff819d04 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQueryEngine.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQueryEngine.ts @@ -15,8 +15,19 @@ */ import { TailwindCSSPalette } from '@finos/legend-art'; -import { PRIMITIVE_TYPE, type V1_AppliedFunction } from '@finos/legend-graph'; -import { IllegalStateError } from '@finos/legend-shared'; +import { + DATE_FORMAT, + PRIMITIVE_TYPE, + type V1_AppliedFunction, +} from '@finos/legend-graph'; +import { + guaranteeNonNullable, + IllegalStateError, + UnsupportedOperationError, + formatDate, +} from '@finos/legend-shared'; +import type { DataCubeQueryFilterOperation } from './filter/DataCubeQueryFilterOperation.js'; +import type { DataCubeQueryAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js'; export enum DataCubeFunction { // relation @@ -25,10 +36,10 @@ export enum DataCubeFunction { GROUP_BY = 'meta::pure::functions::relation::groupBy', LIMIT = 'meta::pure::functions::relation::limit', PIVOT = 'meta::pure::functions::relation::pivot', - // RENAME = 'meta::pure::functions::relation::rename', SELECT = 'meta::pure::functions::relation::select', SLICE = 'meta::pure::functions::relation::slice', SORT = 'meta::pure::functions::relation::sort', + OVER = 'meta::pure::functions::relation::over', // generic CAST = 'meta::pure::functions::lang::cast', @@ -75,14 +86,14 @@ export enum DataCubeFunction { } export type DataCubeQueryFunctionMap = { - leafExtend?: V1_AppliedFunction | undefined; + leafExtend?: V1_AppliedFunction[] | undefined; filter?: V1_AppliedFunction | undefined; pivotSort?: V1_AppliedFunction | undefined; pivot?: V1_AppliedFunction | undefined; pivotCast?: V1_AppliedFunction | undefined; groupBy?: V1_AppliedFunction | undefined; groupBySort?: V1_AppliedFunction | undefined; - groupExtend?: V1_AppliedFunction | undefined; + groupExtend?: V1_AppliedFunction[] | undefined; select?: V1_AppliedFunction | undefined; sort?: V1_AppliedFunction | undefined; limit?: V1_AppliedFunction | undefined; @@ -146,21 +157,6 @@ export enum DataCubeOperationAdvancedValueType { // PARAMETER } -export function isPrimitiveType(type: string) { - return ( - [ - PRIMITIVE_TYPE.NUMBER, - PRIMITIVE_TYPE.INTEGER, - PRIMITIVE_TYPE.DECIMAL, - PRIMITIVE_TYPE.FLOAT, - PRIMITIVE_TYPE.DATE, - PRIMITIVE_TYPE.STRICTDATE, - PRIMITIVE_TYPE.DATETIME, - PRIMITIVE_TYPE.STRING, - ] as string[] - ).includes(type); -} - export type DataCubeOperationValue = { value?: unknown; type: string; @@ -272,48 +268,8 @@ export enum DataCubeColumnDataType { TIME = 'Time', } -export function getDataType(type: string): DataCubeColumnDataType { - switch (type) { - case PRIMITIVE_TYPE.NUMBER: - case PRIMITIVE_TYPE.INTEGER: - case PRIMITIVE_TYPE.DECIMAL: - case PRIMITIVE_TYPE.FLOAT: - return DataCubeColumnDataType.NUMBER; - case PRIMITIVE_TYPE.DATE: - case PRIMITIVE_TYPE.STRICTDATE: - return DataCubeColumnDataType.DATE; - case PRIMITIVE_TYPE.DATETIME: - return DataCubeColumnDataType.TIME; - case PRIMITIVE_TYPE.STRING: - default: - return DataCubeColumnDataType.TEXT; - } -} - -export function ofDataType( - type: string, - dataTypes: DataCubeColumnDataType[], -): boolean { - return dataTypes.includes(getDataType(type)); -} - export const PIVOT_COLUMN_NAME_VALUE_SEPARATOR = '__|__'; -export function isPivotResultColumnName(columnName: string) { - return columnName.includes(PIVOT_COLUMN_NAME_VALUE_SEPARATOR); -} -export function getPivotResultColumnBaseColumnName(columnName: string) { - if (!isPivotResultColumnName(columnName)) { - throw new IllegalStateError( - `Column '${columnName}' is not a pivot result column`, - ); - } - return columnName.substring( - columnName.lastIndexOf(PIVOT_COLUMN_NAME_VALUE_SEPARATOR) + - PIVOT_COLUMN_NAME_VALUE_SEPARATOR.length, - ); -} - export const TREE_COLUMN_VALUE_SEPARATOR = '__/__'; export const DEFAULT_LAMBDA_VARIABLE_NAME = 'x'; @@ -321,6 +277,8 @@ export const DEFAULT_REPORT_NAME = 'New Report'; export const DEFAULT_TREE_COLUMN_SORT_DIRECTION = DataCubeQuerySortDirection.ASCENDING; export const DEFAULT_PIVOT_STATISTIC_COLUMN_NAME = 'Total'; +export const DEFAULT_PIVOT_COLUMN_SORT_DIRECTION = + DataCubeQuerySortDirection.ASCENDING; export const DEFAULT_ROOT_AGGREGATION_COLUMN_VALUE = '[ROOT]'; export const DEFAULT_URL_LABEL_QUERY_PARAM = 'dataCube.linkLabel'; @@ -354,3 +312,105 @@ export const DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; export const TYPEAHEAD_SEARCH_MINIMUM_SEARCH_LENGTH = 3; export const TYPEAHEAD_SEARCH_LIMIT = 10; export const EMPTY_VALUE_PLACEHOLDER = '(None)'; + +// --------------------------------- UTILITIES --------------------------------- + +export function _defaultPrimitiveTypeValue(type: string): unknown { + switch (type) { + case PRIMITIVE_TYPE.STRING: + return ''; + case PRIMITIVE_TYPE.BOOLEAN: + return false; + case PRIMITIVE_TYPE.BYTE: + return btoa(''); + case PRIMITIVE_TYPE.NUMBER: + case PRIMITIVE_TYPE.DECIMAL: + case PRIMITIVE_TYPE.FLOAT: + case PRIMITIVE_TYPE.INTEGER: + case PRIMITIVE_TYPE.BINARY: + return 0; + case PRIMITIVE_TYPE.DATE: + case PRIMITIVE_TYPE.STRICTDATE: + return formatDate(new Date(Date.now()), DATE_FORMAT); + case PRIMITIVE_TYPE.DATETIME: + return formatDate(new Date(Date.now()), DATE_TIME_FORMAT); + default: + throw new UnsupportedOperationError( + `Can't generate value for type '${type}'`, + ); + } +} + +export function getFilterOperation( + operator: string, + operators: DataCubeQueryFilterOperation[], +) { + return guaranteeNonNullable( + operators.find((op) => op.operator === operator), + `Can't find filter operation '${operator}'`, + ); +} + +export function getAggregateOperation( + operator: string, + aggregateOperations: DataCubeQueryAggregateOperation[], +) { + return guaranteeNonNullable( + aggregateOperations.find((op) => op.operator === operator), + `Can't find aggregate operation '${operator}'`, + ); +} + +export function getDataType(type: string): DataCubeColumnDataType { + switch (type) { + case PRIMITIVE_TYPE.NUMBER: + case PRIMITIVE_TYPE.INTEGER: + case PRIMITIVE_TYPE.DECIMAL: + case PRIMITIVE_TYPE.FLOAT: + return DataCubeColumnDataType.NUMBER; + case PRIMITIVE_TYPE.DATE: + case PRIMITIVE_TYPE.STRICTDATE: + return DataCubeColumnDataType.DATE; + case PRIMITIVE_TYPE.DATETIME: + return DataCubeColumnDataType.TIME; + case PRIMITIVE_TYPE.STRING: + default: + return DataCubeColumnDataType.TEXT; + } +} + +export function ofDataType( + type: string, + dataTypes: DataCubeColumnDataType[], +): boolean { + return dataTypes.includes(getDataType(type)); +} + +export function isPrimitiveType(type: string) { + return ( + [ + PRIMITIVE_TYPE.NUMBER, + PRIMITIVE_TYPE.INTEGER, + PRIMITIVE_TYPE.DECIMAL, + PRIMITIVE_TYPE.FLOAT, + PRIMITIVE_TYPE.DATE, + PRIMITIVE_TYPE.STRICTDATE, + PRIMITIVE_TYPE.DATETIME, + PRIMITIVE_TYPE.STRING, + ] as string[] + ).includes(type); +} +export function isPivotResultColumnName(columnName: string) { + return columnName.includes(PIVOT_COLUMN_NAME_VALUE_SEPARATOR); +} +export function getPivotResultColumnBaseColumnName(columnName: string) { + if (!isPivotResultColumnName(columnName)) { + throw new IllegalStateError( + `Column '${columnName}' is not a pivot result column`, + ); + } + return columnName.substring( + columnName.lastIndexOf(PIVOT_COLUMN_NAME_VALUE_SEPARATOR) + + PIVOT_COLUMN_NAME_VALUE_SEPARATOR.length, + ); +} diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshot.ts b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshot.ts index e54aa45fd3..fcab57709a 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshot.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshot.ts @@ -51,6 +51,11 @@ export type DataCubeQuerySnapshotExtendedColumn = DataCubeColumn & { reduceFn?: PlainObject | undefined; }; +export type DataCubeQuerySnapshotAggregateColumn = DataCubeColumn & { + parameterValues: DataCubeOperationValue[]; + operator: string; +}; + export type DataCubeQuerySnapshotSortColumn = DataCubeColumn & { direction: DataCubeQuerySortDirection; }; @@ -64,6 +69,14 @@ export type DataCubeQuerySnapshotPivot = { castColumns: DataCubeColumn[]; }; +export type DataCubeQuerySnapshotProcessingContext = { + snapshot: DataCubeQuerySnapshot; + pivotAggColumns: DataCubeQuerySnapshotAggregateColumn[]; + pivotSortColumns: DataCubeQuerySnapshotSortColumn[]; + groupByAggColumns: DataCubeQuerySnapshotAggregateColumn[]; + groupBySortColumns: DataCubeQuerySnapshotSortColumn[]; +}; + export type DataCubeQuerySnapshotData = { configuration: PlainObject; sourceColumns: DataCubeColumn[]; diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilder.ts b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilder.ts index cb8e27da22..e7b62b976e 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilder.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilder.ts @@ -25,44 +25,81 @@ import { V1_AppliedFunction, V1_CInteger, - V1_Collection, extractElementNameFromPath as _name, matchFunctionName, type V1_ValueSpecification, V1_Lambda, } from '@finos/legend-graph'; import type { DataCubeQuery } from './model/DataCubeQuery.js'; -import { DataCubeQuerySnapshot } from './DataCubeQuerySnapshot.js'; -import { _toCol, type DataCubeColumn } from './model/DataCubeColumn.js'; -import { assertTrue, at, guaranteeNonNullable } from '@finos/legend-shared'; import { - DataCubeQuerySortDirection, + DataCubeQuerySnapshot, + type DataCubeQuerySnapshotAggregateColumn, + type DataCubeQuerySnapshotProcessingContext, + type DataCubeQuerySnapshotSortColumn, +} from './DataCubeQuerySnapshot.js'; +import { + _findCol, + _toCol, + type DataCubeColumn, +} from './model/DataCubeColumn.js'; +import { + assertTrue, + at, + guaranteeNonNullable, + isNonNullable, +} from '@finos/legend-shared'; +import { DataCubeFunction, type DataCubeQueryFunctionMap, } from './DataCubeQueryEngine.js'; -import { buildDefaultConfiguration } from './DataCubeConfigurationBuilder.js'; +import { newConfiguration } from './DataCubeConfigurationBuilder.js'; import { _colSpecArrayParam, - _colSpecParam, - _funcMatch, _param, _extractExtendedColumns, _filter, _relationType, _genericTypeParam, _packageableType, + _aggCol, + _sort, + _unwrapLambda, + _pivotSort, + _groupBySort, + _validatePivot, + _checkDuplicateColumns, + _validateGroupBy, } from './DataCubeQuerySnapshotBuilderUtils.js'; import type { DataCubeSource } from './model/DataCubeSource.js'; -import type { DataCubeQueryFilterOperation } from './filter/DataCubeQueryFilterOperation.js'; -import type { DataCubeQueryAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js'; +import type { DataCubeEngine } from './DataCubeEngine.js'; +import { + _deserializeValueSpecification, + _serializeValueSpecification, +} from './DataCubeQueryBuilderUtils.js'; // --------------------------------- BUILDING BLOCKS --------------------------------- const _SUPPORTED_TOP_LEVEL_FUNCTIONS: { func: string; + /** + * If there are multiple signature to a function, such as extend(), this indicates + * the minimum number of parameters. And a stopping condition needs to be provided. + * + * Note that this is a naive mechanism to process simple functions, by no means, it's + * meant to mimic generic function matcher. + */ parameters: number; + stopCondition?: ((_func: V1_AppliedFunction) => boolean) | undefined; }[] = [ - { func: DataCubeFunction.EXTEND, parameters: 2 }, // TODO: support both signatures of extend() + { + func: DataCubeFunction.EXTEND, + parameters: 1, + // handle OLAP form where first parameter is over() expression used to construct the window + stopCondition: (_func) => + matchFunctionName(_func.function, DataCubeFunction.EXTEND) && + _func.parameters[0] instanceof V1_AppliedFunction && + matchFunctionName(_func.parameters[0].function, DataCubeFunction.OVER), + }, { func: DataCubeFunction.FILTER, parameters: 1 }, { func: DataCubeFunction.SELECT, parameters: 1 }, { func: DataCubeFunction.GROUP_BY, parameters: 2 }, @@ -102,14 +139,6 @@ enum _FUNCTION_SEQUENCE_COMPOSITION_PART { LIMIT = 'limit', } -// function isFunctionInValidSequence( -// value: string, -// ): value is _FUNCTION_SEQUENCE_COMPOSITION_PART { -// return Object.values(_FUNCTION_SEQUENCE_COMPOSITION_PART).includes( -// value as _FUNCTION_SEQUENCE_COMPOSITION_PART, -// ); -// } - // This corresponds to the function sequence that we currently support: // // ->extend()* @@ -199,10 +228,10 @@ const _FUNCTION_SEQUENCE_COMPOSITION_PATTERN_REGEXP = new RegExp( ? `(${node.funcs .map( (childNode) => - `(?<${childNode.name}><${_name(childNode.func)}>____\\d+)${childNode.repeat ? '*' : !childNode.required ? '?' : ''}`, + `(?<${childNode.name}>${childNode.repeat ? `(?:<${_name(childNode.func)}>____\\d+)*` : `<${_name(childNode.func)}>____\\d+`})${childNode.repeat ? '' : !childNode.required ? '?' : ''}`, ) .join('')})${node.repeat ? '*' : !node.required ? '?' : ''}` - : `(?<${node.name}><${_name(node.func)}>____\\d+)${node.repeat ? '*' : !node.required ? '?' : ''}`, + : `(?<${node.name}>${node.repeat ? `(?:<${_name(node.func)}>____\\d+)*` : `<${_name(node.func)}>____\\d+`})${node.repeat ? '' : !node.required ? '?' : ''}`, ) .join('')}$`, ); @@ -216,7 +245,7 @@ function extractFunctionMap( ): DataCubeQueryFunctionMap { // Make sure this is a sequence of function calls if (!(query instanceof V1_AppliedFunction)) { - throw new Error(`Can't process expression: Expected a function expression`); + throw new Error(`Can't process expression: expected a function expression`); } const sequence: V1_AppliedFunction[] = []; let currentFunc = query; @@ -229,7 +258,7 @@ function extractFunctionMap( // Check that all functions in sequence are supported (matching name and number of parameters) if (!supportedFunc) { throw new Error( - `Can't process expression: Found unsupported function ${currentFunc.function}()`, + `Can't process expression: found unsupported function ${currentFunc.function}()`, ); } @@ -238,16 +267,22 @@ function extractFunctionMap( // t(...)->z()->y()->x() and simultaneously, remove the first parameter from each function for // simplicity, except for the innermost function if (currentFunc.parameters.length > supportedFunc.parameters) { + // if stop condition is fulfilled, no more function sequence drilling is needed + if (supportedFunc.stopCondition?.(currentFunc)) { + sequence.unshift(currentFunc); + break; + } + // assert that the supported function has the expected number of parameters assertTrue( currentFunc.parameters.length === supportedFunc.parameters + 1, - `Can't process ${_name(currentFunc.function)}() expression: Expected at most ${supportedFunc.parameters + 1} parameters provided, got ${currentFunc.parameters.length}`, + `Can't process ${_name(currentFunc.function)}() expression: expected at most ${supportedFunc.parameters + 1} parameters provided, got ${currentFunc.parameters.length}`, ); const func = _param( currentFunc, 0, V1_AppliedFunction, - `Can't process expression: Expected a sequence of function calls (e.g. x()->y()->z())`, + `Can't process expression: expected a sequence of function calls (e.g. x()->y()->z())`, ); currentFunc.parameters = currentFunc.parameters.slice(1); sequence.unshift(currentFunc); @@ -267,35 +302,46 @@ function extractFunctionMap( ); if (!matchResult) { throw new Error( - `Can't process expression: Unsupported function composition ${sequence.map((fn) => `${_name(fn.function)}()`).join('->')} (supported composition: ${_FUNCTION_SEQUENCE_COMPOSITION_PATTERN.map((node) => `${'funcs' in node ? `[${node.funcs.map((childNode) => `${_name(childNode.func)}()`).join('->')}]` : `${_name(node.func)}()`}`).join('->')})`, + `Can't process expression: unsupported function composition ${sequence.map((fn) => `${_name(fn.function)}()`).join('->')} (supported composition: ${_FUNCTION_SEQUENCE_COMPOSITION_PATTERN.map((node) => `${'funcs' in node ? `[${node.funcs.map((childNode) => `${_name(childNode.func)}()`).join('->')}]` : `${_name(node.func)}()`}`).join('->')})`, ); } - const _process = (key: string): V1_AppliedFunction | undefined => { + const _process = (key: string): V1_AppliedFunction[] | undefined => { const match = matchResult.groups?.[key]; - if (!match || match.indexOf('____') === -1) { + if (!match) { return undefined; } - const idx = Number(match.split('____')[1]); - if (isNaN(idx) || idx >= sequence.length) { + const funcMatches = match.match(/\<.*?\>____\d+/g); + if (!funcMatches?.length) { return undefined; } - const func = at(sequence, idx); - return func; + + return funcMatches + .map((funcMatch) => { + const idx = Number(funcMatch.split('____')[1]); + if (isNaN(idx) || idx >= sequence.length) { + return undefined; + } + const func = at(sequence, idx); + return func; + }) + .filter(isNonNullable); }; return { leafExtend: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.LEAF_EXTEND), - select: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.SELECT), - filter: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.FILTER), - pivotSort: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT_SORT), - pivot: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT), - pivotCast: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT_CAST), - groupBy: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.GROUP_BY), - groupBySort: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.GROUP_BY_SORT), + select: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.SELECT)?.[0], + filter: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.FILTER)?.[0], + pivotSort: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT_SORT)?.[0], + pivot: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT)?.[0], + pivotCast: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.PIVOT_CAST)?.[0], + groupBy: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.GROUP_BY)?.[0], + groupBySort: _process( + _FUNCTION_SEQUENCE_COMPOSITION_PART.GROUP_BY_SORT, + )?.[0], groupExtend: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.GROUP_EXTEND), - sort: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.SORT), - limit: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.LIMIT), + sort: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.SORT)?.[0], + limit: _process(_FUNCTION_SEQUENCE_COMPOSITION_PART.LIMIT)?.[0], }; } @@ -307,31 +353,34 @@ function extractFunctionMap( * Implementation-wise, this extracts the function call sequence, then walk the * sequence in order to fill in the information for the snapshot. */ -export function validateAndBuildQuerySnapshot( +export async function validateAndBuildQuerySnapshot( partialQuery: V1_ValueSpecification, source: DataCubeSource, baseQuery: DataCubeQuery, - filterOperations: DataCubeQueryFilterOperation[], - aggregateOperations: DataCubeQueryAggregateOperation[], + engine: DataCubeEngine, ) { // --------------------------------- BASE --------------------------------- // Build the function call sequence and the function map to make the // analysis more ergonomic - const funcMap = extractFunctionMap(partialQuery); + // Clone the query since we will mutate it during the process + const query = _deserializeValueSpecification( + _serializeValueSpecification(partialQuery), + ); + const funcMap = extractFunctionMap(query); const snapshot = DataCubeQuerySnapshot.create({}); const data = snapshot.data; - const columnNames = new Set(); + const registeredColumns = new Map(); /** * We want to make sure all columns, either from source or created, e.g. extended columns, * have unique names. This is to simplify the logic within DataCube so different components * can easily refer to columns by name without having to worry about conflicts. */ const _checkColName = (col: DataCubeColumn, message: string) => { - if (columnNames.has(col.name)) { + if (registeredColumns.has(col.name)) { throw new Error(message); } - columnNames.add(col.name); + registeredColumns.set(col.name, col); }; const colsMap = new Map(); const _getCol = (colName: string) => { @@ -342,53 +391,66 @@ export function validateAndBuildQuerySnapshot( return _toCol(column); }; const _setCol = (col: DataCubeColumn) => colsMap.set(col.name, col); - // const _unsetCol = (col: DataCubeColumn) => colsMap.delete(col.name); // -------------------------------- SOURCE -------------------------------- data.sourceColumns = source.columns; - data.sourceColumns.forEach((col) => _setCol(col)); + + // validate + _checkDuplicateColumns( + data.sourceColumns, + (colName) => + `Can't process source: found duplicate source columns '${colName}'`, + ); data.sourceColumns.forEach((col) => _checkColName( col, - `Can't process source column '${col.name}': another column with the same name is already registered`, + `Can't process source: another column with name '${col.name}' is already registered`, ), ); + data.sourceColumns.forEach((col) => _setCol(col)); + // --------------------------- LEAF-LEVEL EXTEND --------------------------- - if (funcMap.leafExtend) { - // TODO: get column types (call engine to get the type) - data.leafExtendedColumns = _extractExtendedColumns(funcMap.leafExtend); - // TODO: populate the column types - data.leafExtendedColumns.forEach((col) => _setCol(col)); + if (funcMap.leafExtend?.length) { + data.leafExtendedColumns = await _extractExtendedColumns( + funcMap.leafExtend, + Array.from(colsMap.values()), + engine, + ); + + // validate + // NOTE: these duplication checks might not be necessary since compiler would catch these + // issues anyway, but we leave them here to be defensive + _checkDuplicateColumns( + data.leafExtendedColumns, + (colName) => + `Can't process extend() expression: found duplicate extended columns '${colName}'`, + ); data.leafExtendedColumns.forEach((col) => _checkColName( col, - `Can't process leaf-level extended column '${col.name}': another column with the same name is already registered`, + `Can't process extend() expression: another column with name '${col.name}' is already registered`, ), ); + + data.leafExtendedColumns.forEach((col) => _setCol(col)); } // --------------------------------- FILTER --------------------------------- if (funcMap.filter) { - // TODO: verify column presence - // TODO: verify column types const lambda = _param( funcMap.filter, 0, V1_Lambda, - `Can't process filter() expression: Expected parameter at index 0 to be a lambda expression`, - ); - assertTrue( - lambda.body.length === 1, - `Can't process filter() expression: Expected lambda body to have exactly 1 expression`, + `Can't process filter() expression: expected parameter at index 0 to be a lambda expression`, ); data.filter = _filter( - guaranteeNonNullable(lambda.body[0]), + _unwrapLambda(lambda, `Can't process filter() expression`), _getCol, - filterOperations, + engine.filterOperations, ); } @@ -398,119 +460,156 @@ export function validateAndBuildQuerySnapshot( data.selectColumns = _colSpecArrayParam(funcMap.select, 0).colSpecs.map( (colSpec) => _getCol(colSpec.name), ); - // TODO: remove columns that are not selected from colsMap + + // validate + _checkDuplicateColumns( + data.selectColumns, + (colName) => + `Can't process select() expression: found duplicate select columns '${colName}'`, + ); + + // restrict the set of available columns to only selected columns + colsMap.clear(); + data.selectColumns.forEach((col) => _setCol(col)); + } else { + // mandate that if select() expression is not present, we consider this + // as no-column is selected + colsMap.clear(); } // --------------------------------- PIVOT --------------------------------- - if (funcMap.pivot && funcMap.pivotCast) { - // TODO: verify column presence - // TODO: verify column types + let pivotAggColumns: DataCubeQuerySnapshotAggregateColumn[] = []; + let pivotSortColumns: DataCubeQuerySnapshotSortColumn[] = []; + if (funcMap.pivot && funcMap.pivotCast && funcMap.pivotSort) { + const pivotColumns = _colSpecArrayParam(funcMap.pivot, 0).colSpecs.map( + (colSpec) => _getCol(colSpec.name), + ); + const castColumns = _relationType( + _genericTypeParam(funcMap.pivotCast, 0).genericType, + ).columns.map((column) => ({ + name: column.name, + type: _packageableType(column.genericType).fullPath, + })); data.pivot = { - columns: _colSpecArrayParam(funcMap.pivot, 0).colSpecs.map((colSpec) => - _getCol(colSpec.name), - ), - castColumns: _relationType( - _genericTypeParam(funcMap.pivotCast, 0).genericType, - ).columns.map((column) => ({ - name: column.name, - type: _packageableType(column.genericType).fullPath, - })), + columns: pivotColumns, + castColumns: castColumns, }; + + // process aggregate columns + pivotAggColumns = _colSpecArrayParam(funcMap.pivot, 1).colSpecs.map( + (colSpec) => _aggCol(colSpec, _getCol, engine.aggregateOperations), + ); + // process sort columns + pivotSortColumns = _pivotSort(funcMap.pivotSort, pivotColumns, _getCol); + + // validate + _validatePivot(data.pivot, pivotAggColumns, Array.from(colsMap.values())); + + // restrict the set of available columns to only casted columns + colsMap.clear(); + castColumns.forEach((col) => _setCol(col)); } - // TODO: verify groupBy agg columns, pivot agg columns and configuration agree // --------------------------------- GROUP BY --------------------------------- - if (funcMap.groupBy) { - // TODO: verify column presence - // TODO: verify column types + let groupByAggColumns: DataCubeQuerySnapshotAggregateColumn[] = []; + let groupBySortColumns: DataCubeQuerySnapshotSortColumn[] = []; + if (funcMap.groupBy && funcMap.groupBySort) { + const groupByColumns = _colSpecArrayParam(funcMap.groupBy, 0).colSpecs.map( + (colSpec) => _getCol(colSpec.name), + ); data.groupBy = { - columns: _colSpecArrayParam(funcMap.groupBy, 0).colSpecs.map((colSpec) => - _getCol(colSpec.name), - ), + columns: groupByColumns, }; - // TODO: verify groupBy agg columns, pivot agg columns and configuration agree - // TODO: verify sort columns - // TODO: use configuration information present in the baseQuery configuration? + + // process aggregate columns + groupByAggColumns = _colSpecArrayParam(funcMap.groupBy, 1).colSpecs.map( + (colSpec) => _aggCol(colSpec, _getCol, engine.aggregateOperations), + ); + // process sort columns + groupBySortColumns = _groupBySort( + funcMap.groupBySort, + groupByColumns, + _getCol, + ); + + // validate + _validateGroupBy( + data.groupBy, + groupByAggColumns, + data.pivot, + pivotAggColumns, + Array.from(colsMap.values()), + ); } // --------------------------- GROUP-LEVEL EXTEND --------------------------- - if (funcMap.groupExtend) { - // TODO: get column types (call engine to get the type) - data.groupExtendedColumns = _extractExtendedColumns(funcMap.groupExtend); - // TODO: populate the column types - data.groupExtendedColumns.forEach((col) => _setCol(col)); + if (funcMap.groupExtend?.length) { + data.groupExtendedColumns = await _extractExtendedColumns( + funcMap.groupExtend, + Array.from(colsMap.values()), + engine, + ); + + // validate + // NOTE: these duplication checks might not be necessary since compiler would catch these + // issues anyway, but we leave them here to be defensive + _checkDuplicateColumns( + data.groupExtendedColumns, + (colName) => + `Can't process extend() expression: found duplicate extended columns '${colName}'`, + ); data.groupExtendedColumns.forEach((col) => _checkColName( col, - `Can't process group-level extended column '${col.name}': another column with the same name is already registered`, + `Can't process extend() expression: another column with name '${col.name}' is already registered`, ), ); + + data.groupExtendedColumns.forEach((col) => _setCol(col)); } // --------------------------------- SORT --------------------------------- if (funcMap.sort) { - data.sortColumns = _param(funcMap.sort, 0, V1_Collection).values.map( - (value) => { - const sortColFunc = _funcMatch(value, [ - DataCubeFunction.ASCENDING, - DataCubeFunction.DESCENDING, - ]); - return { - ..._getCol(_colSpecParam(sortColFunc, 0).name), - direction: matchFunctionName( - sortColFunc.function, - DataCubeFunction.ASCENDING, - ) - ? DataCubeQuerySortDirection.ASCENDING - : DataCubeQuerySortDirection.DESCENDING, - }; - }, + data.sortColumns = _sort(funcMap.sort, _getCol); + + // validate + _checkDuplicateColumns( + data.sortColumns, + (colName) => + `Can't process sort() expression: found duplicate sort columns '${colName}'`, ); } // --------------------------------- LIMIT --------------------------------- if (funcMap.limit) { + // NOTE: negative number -10 is parsed as minus(10) so this check will also + // reject negative number const value = _param( funcMap.limit, 0, V1_CInteger, - `Can't process limit() expression: Expected parameter at index 0 to be an integer instance value`, + `Can't process limit() expression: expected limit to be a non-negative integer value`, ); data.limit = value.value; } // --------------------------------- CONFIGURATION --------------------------------- - // - // A data-cube conceptually consists of a data query, in form of a Pure query, instead - // of a particular specification object format, and this configuration, that holds mostly - // layout and styling customization. But there are overlaps, i.e. certain "meta" query - // configuration are stored in this configuration, e.g. column aggregation type, because - // a column aggregation's preference needs to be specified even when there's no aggregation - // specified over that column in the data query. - // - // But in the example above, if the column is part of an aggregation, we have to ensure - // the configuration is consistent with the query. Conflicts can happen, for example: - // - column kind and type conflict with aggregation - // - column kind and type conflict with the column configuration - // - // In those cases, we need to reconcile to make sure the query and the configuration to agree. - // The query will take precedence when conflicts happen, and if the conflict cannot be resolved - // somehow, we will throw a validation error. We decide so because in certain cases, configuration - // needs to be generated from default presets, such as for use cases where the query comes from a - // different source, such as Studio or Query, or another part of Engine, where the layout - // configuration is not specified. - // - // ---------------------------------------------------------------------------------- const configuration = validateAndBuildConfiguration( - snapshot, - funcMap, + { + snapshot, + groupByAggColumns, + groupBySortColumns, + pivotAggColumns, + pivotSortColumns, + }, baseQuery, + engine, ); data.configuration = configuration.serialize(); @@ -518,32 +617,155 @@ export function validateAndBuildQuerySnapshot( } /** - * TODO: @datacube roundtrip - implement the logic to reconcile the configuration with the query - * - [ ] columns (missing/extra columns - remove or generate default column configuration) - * - [ ] base off the type and kind, check the settings to see if it's compatible - * - [ ] verify groupBy agg columns, pivot agg columns and configuration agree - * - [ ] verify groupBy sort columns and tree column sort direction configuration agree + * Builds and/or validates the configuration. + * + * TL;DR; + * If not provided, generate a default configuration based off the metadata extracted + * when processing the query in previous steps. + * If provided, check if the configuration aggree with the query processing metadata. + * + * CONTEXT: + * A data-cube conceptually consists of a data query, in form of a Pure query, instead + * of a particular specification object format, and this configuration, that holds mostly + * layout and styling customization. But there are overlaps, i.e. certain _meta_ query + * configuration are stored in this configuration, e.g. column aggregation type, because + * a column aggregation's preference needs to be specified even when there's no aggregation + * specified over that column in the data query. + * + * But in the example above, if the column is part of an aggregation, we have to ensure + * the configuration is consistent with the query. Conflicts can happen, for example: + * - column kind and type conflict with aggregation + * - column kind and type conflict with the column configuration + * + * In those cases, we need to make sure the query and the configuration to agree. + * If a config is provided, we will need to validate that config. If none is provided, + * we will generate a config from the query processing metadata, in which case, no + * validation is needed. The latter case comes up quite often where the query comes from a + * different source, such as Studio or Query, or another part of Engine, and the layout + * configuration is not specified. */ function validateAndBuildConfiguration( - snapshot: DataCubeQuerySnapshot, - funcMap: DataCubeQueryFunctionMap, + context: DataCubeQuerySnapshotProcessingContext, baseQuery: DataCubeQuery, + engine: DataCubeEngine, ) { - const data = snapshot.data; - const configuration = - baseQuery.configuration?.clone() ?? - buildDefaultConfiguration([ - ...data.sourceColumns, - ...data.leafExtendedColumns, - ...data.groupExtendedColumns, - ]); - - // column selection - configuration.columns.forEach((column) => { - column.isSelected = Boolean( - data.groupExtendedColumns.find((col) => col.name === column.name) ?? - data.selectColumns.find((col) => col.name === column.name), + const data = context.snapshot.data; + const config = baseQuery.configuration; + // generate a default configuration anyway to be used to compare with the + // provided configuration for validation purpose + const _config = newConfiguration(context); + + if (!config) { + return _config; + } + + // check tree column sort direction (only relevant if groupBy is present) + if (data.groupBy) { + assertTrue( + config.treeColumnSortDirection === _config.treeColumnSortDirection, + `Can't process configuration: tree column sort direction mismatch (expected: '${_config.treeColumnSortDirection.toLowerCase()}', found: '${config.treeColumnSortDirection.toLowerCase()}')`, + ); + } + + // check columns + const columns = config.columns; + const _columns = _config.columns; + const columnNames = new Set(); + + // check for duplicate columns + columns.forEach((col) => { + if (columnNames.has(col.name)) { + throw new Error( + `Can't process configuration: found duplicate columns '${col.name}'`, + ); + } else { + columnNames.add(col.name); + } + }); + + // check for extra columns + columns.forEach((col) => { + if (!_findCol(_columns, col.name)) { + throw new Error( + `Can't process configuration: found extra column '${col.name}'`, + ); + } + }); + + // check for missing columns + _columns.forEach((col) => { + if (!_findCol(columns, col.name)) { + throw new Error( + `Can't process configuration: missing column '${col.name}'`, + ); + } + }); + + // check for columns ordering + const columnsOrdering = [ + ...data.selectColumns, + ...data.groupExtendedColumns, + ...[...data.sourceColumns, ...data.leafExtendedColumns].filter( + (col) => !_findCol(data.selectColumns, col.name), + ), + ]; + columnsOrdering.forEach((_col, idx) => { + const col = at(columns, idx); + assertTrue( + _col.name === col.name, + `Can't process configuration: column ordering mismatch at index ${idx} (expected: '${_col.name}', found: '${col.name})', expected ordering: ${columnsOrdering.map((c) => c.name).join(', ')}`, + ); + }); + + columns.forEach((column) => { + const _column = guaranteeNonNullable(_findCol(_columns, column.name)); + + // check type + assertTrue( + column.type === _column.type, + `Can't process configuration: type mismatch for column '${column.name}' (expected: '${_column.type}', found: '${column.type}')`, + ); + + // check selection + assertTrue( + column.isSelected === _column.isSelected, + `Can't process configuration: selection mismatch for column '${column.name}' (expected: '${_column.isSelected}', found: '${column.isSelected}')`, ); + + // check kind (only relevant if aggregation is present) + if (data.pivot ?? data.groupBy) { + assertTrue( + column.kind === _column.kind, + `Can't process configuration: kind mismatch for column '${column.name}' (expected: '${_column.kind.toLowerCase()}', found: '${column.kind.toLowerCase()}')`, + ); + } + + // check aggregation (only relevant if aggregation is present) + if (data.pivot ?? data.groupBy) { + assertTrue( + column.aggregateOperator === _column.aggregateOperator, + `Can't process configuration: aggregation operator mismatch for column '${column.name}' (expected: '${_column.aggregateOperator}', found: '${column.aggregateOperator}')`, + ); + assertTrue( + engine + .getAggregateOperation(column.aggregateOperator) + .isCompatibleWithParameterValues(column.aggregationParameters), + `Can't process configuration: incompatible aggregation parameter values for column '${column.name}' (operator: '${column.aggregateOperator}')`, + ); + } + + // check pivot sort direction and exclusion (only relevant if pivot is present) + if (data.pivot) { + assertTrue( + column.excludedFromPivot === _column.excludedFromPivot, + `Can't process configuration: pivot exclusion mismatch for column '${column.name}' (expected: '${_column.excludedFromPivot}', found: '${column.excludedFromPivot}')`, + ); + assertTrue( + column.pivotSortDirection === _column.pivotSortDirection, + `Can't process configuration: pivot sort direction mismatch for column '${column.name}' (expected: '${_column.pivotSortDirection?.toLowerCase() ?? 'none'}', found: '${column.pivotSortDirection?.toLowerCase() ?? 'none'}')`, + ); + } }); - return configuration; + + return config; } diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilderUtils.ts b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilderUtils.ts index a3472a1180..de17d35bbc 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilderUtils.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQuerySnapshotBuilderUtils.ts @@ -29,6 +29,7 @@ import { V1_ColSpec, V1_ColSpecArray, V1_GenericTypeInstance, + V1_Variable, type V1_ValueSpecification, V1_PrimitiveValueSpecification, extractElementNameFromPath as _name, @@ -36,15 +37,18 @@ import { V1_RelationType, V1_PackageableType, type V1_GenericType, + V1_Collection, + type V1_Lambda, } from '@finos/legend-graph'; -import { type DataCubeColumn } from './model/DataCubeColumn.js'; +import { _findCol, type DataCubeColumn } from './model/DataCubeColumn.js'; import { + assertErrorThrown, assertTrue, assertType, at, + deepEqual, guaranteeNonNullable, guaranteeType, - IllegalStateError, uniq, UnsupportedOperationError, type Clazz, @@ -53,19 +57,60 @@ import { DataCubeFunction, DataCubeOperationAdvancedValueType, DataCubeQueryFilterGroupOperator, + DataCubeQuerySortDirection, + DEFAULT_LAMBDA_VARIABLE_NAME, getDataType, + getPivotResultColumnBaseColumnName, + isPivotResultColumnName, TREE_COLUMN_VALUE_SEPARATOR, type DataCubeOperationValue, } from './DataCubeQueryEngine.js'; import type { + DataCubeQuerySnapshotAggregateColumn, DataCubeQuerySnapshotFilter, DataCubeQuerySnapshotFilterCondition, + DataCubeQuerySnapshotGroupBy, + DataCubeQuerySnapshotPivot, } from './DataCubeQuerySnapshot.js'; -import { _serializeValueSpecification } from './DataCubeQueryBuilderUtils.js'; import type { DataCubeQueryFilterOperation } from './filter/DataCubeQueryFilterOperation.js'; +import type { DataCubeEngine } from './DataCubeEngine.js'; +import { + _cols, + _colSpec, + _function, + _lambda, + _serializeValueSpecification, + _synthesizeMinimalSourceQuery, +} from './DataCubeQueryBuilderUtils.js'; +import { INTERNAL__DataCubeSource } from './model/DataCubeSource.js'; +import type { DataCubeQueryAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js'; // --------------------------------- UTILITIES --------------------------------- +export function _var(variable: V1_Variable) { + assertTrue( + variable.name === DEFAULT_LAMBDA_VARIABLE_NAME, + `Can't process variable '${variable.name}': expected variable name to be '${DEFAULT_LAMBDA_VARIABLE_NAME}'`, + ); +} + +export function _propertyCol( + property: V1_AppliedProperty, + columnGetter: (name: string) => DataCubeColumn, +) { + assertTrue( + property.parameters.length === 1, + `Can't process property '${property.property}': expected exactly 1 parameter`, + ); + const variable = guaranteeType( + at(property.parameters, 0), + V1_Variable, + `Can't process property '${property.property}': failed to extract variable`, + ); + _var(variable); + return columnGetter(property.property); +} + export function _param( func: V1_AppliedFunction, paramIdx: number, @@ -74,13 +119,13 @@ export function _param( ): T { assertTrue( func.parameters.length >= paramIdx + 1, - `Can't process ${_name(func.function)}() expression: Expected at least ${paramIdx + 1} parameter(s)`, + `Can't process ${_name(func.function)}() expression: expected at least ${paramIdx + 1} parameter(s)`, ); return guaranteeType( func.parameters[paramIdx], clazz, message ?? - `Can't process ${_name(func.function)}() expression: Found unexpected type for parameter at index ${paramIdx}`, + `Can't process ${_name(func.function)}() expression: found unexpected type for parameter at index ${paramIdx}`, ); } @@ -88,7 +133,7 @@ export function _colSpecParam(func: V1_AppliedFunction, paramIdx: number) { return guaranteeType( _param(func, paramIdx, V1_ClassInstance).value, V1_ColSpec, - `Can't process ${_name(func.function)}() expression: Expected parameter at index ${paramIdx} to be a column specification`, + `Can't process ${_name(func.function)}() expression: expected parameter at index ${paramIdx} to be a column specification`, ); } @@ -96,7 +141,7 @@ export function _colSpecArrayParam(func: V1_AppliedFunction, paramIdx: number) { return guaranteeType( _param(func, paramIdx, V1_ClassInstance).value, V1_ColSpecArray, - `Can't process ${_name(func.function)}() expression: Expected parameter at index ${paramIdx} to be a column specification list`, + `Can't process ${_name(func.function)}() expression: expected parameter at index ${paramIdx} to be a column specification list`, ); } @@ -105,10 +150,23 @@ export function _genericTypeParam(func: V1_AppliedFunction, paramIdx: number) { func, paramIdx, V1_GenericTypeInstance, - `Can't process ${_name(func.function)}: Expected parameter at index ${paramIdx} to be a generic type instance`, + `Can't process ${_name(func.function)}: expected parameter at index ${paramIdx} to be a generic type instance`, ); } +export function _unwrapLambda(lambda: V1_Lambda, message?: string | undefined) { + assertTrue( + lambda.body.length === 1, + `${message ?? `Can't process lambda`}: expected lambda body to have exactly 1 expression`, + ); + assertTrue( + lambda.parameters.length === 1, + `${message ?? `Can't process lambda`}: expected lambda to have exactly 1 parameter`, + ); + _var(at(lambda.parameters, 0)); + return at(lambda.body, 0); +} + export function _funcMatch( value: V1_ValueSpecification | undefined, functionNames: string | string[], @@ -116,14 +174,14 @@ export function _funcMatch( assertType( value, V1_AppliedFunction, - `Can't process function: Found unexpected value specification type`, + `Can't process function: found unexpected value specification type`, ); assertTrue( matchFunctionName( value.function, Array.isArray(functionNames) ? functionNames : [functionNames], ), - `Can't process function: Expected function to be one of [${uniq((Array.isArray(functionNames) ? functionNames : [functionNames]).map(_name)).join(', ')}]`, + `Can't process function: expected function to be one of [${uniq((Array.isArray(functionNames) ? functionNames : [functionNames]).map(_name)).join(', ')}]`, ); return value; } @@ -164,7 +222,50 @@ export function _operationPrimitiveValue( } else if (value instanceof V1_CStrictTime) { return { value: value.value, type: PRIMITIVE_TYPE.STRICTTIME }; } - throw new UnsupportedOperationError(`Can't process primitive value`); + throw new UnsupportedOperationError( + `Can't process unsupported operation primitive value`, + ); +} + +export function _operationValue( + value: V1_ValueSpecification | undefined, + columnGetter: (name: string) => DataCubeColumn, + columnChecker?: ((column: DataCubeColumn) => void) | undefined, +) { + if (value instanceof V1_PrimitiveValueSpecification) { + return _operationPrimitiveValue(value); + } else if (value instanceof V1_AppliedProperty) { + const column = _propertyCol(value, columnGetter); + columnChecker?.(column); + return { + value: column.name, + type: DataCubeOperationAdvancedValueType.COLUMN, + }; + } else if (value === undefined) { + return { + type: DataCubeOperationAdvancedValueType.VOID, + }; + } + throw new UnsupportedOperationError( + `Can't process unsupported operation value`, + ); +} + +export function _checkDuplicateColumns( + columns: DataCubeColumn[], + message?: ((colName: string) => string) | undefined, +) { + const cols = new Set(); + columns.forEach((col) => { + if (cols.has(col.name)) { + throw new Error( + message?.(col.name) ?? + `Can't process expression: found duplicate columns '${col.name}'`, + ); + } else { + cols.add(col.name); + } + }); } // --------------------------------- BUILDING BLOCKS --------------------------------- @@ -186,10 +287,8 @@ export function _pruneExpandedPaths( let lastCommonIndex = -1; for (let i = 0; i < length; i++) { if ( - guaranteeNonNullable(prevGroupByCols[i]).name !== - guaranteeNonNullable(currentGroupByCols[i]).name || - guaranteeNonNullable(prevGroupByCols[i]).type !== - guaranteeNonNullable(currentGroupByCols[i]).type + at(prevGroupByCols, i).name !== at(currentGroupByCols, i).name || + at(prevGroupByCols, i).type !== at(currentGroupByCols, i).type ) { break; } @@ -203,58 +302,74 @@ export function _pruneExpandedPaths( .sort(); } -export function _extractExtendedColumns(func: V1_AppliedFunction) { - const extendFuncs: V1_AppliedFunction[] = []; - let currentFunc = func; - - while (currentFunc instanceof V1_AppliedFunction) { - // since we are processing a chain of extend(), we can finish processing when - // encountering a different function. - if (!matchFunctionName(currentFunc.function, DataCubeFunction.EXTEND)) { - break; - } - - if (currentFunc.parameters.length === 2) { - const valueSpecification = currentFunc.parameters[0]; - if (!(valueSpecification instanceof V1_AppliedFunction)) { - throw new IllegalStateError( - `Can't process extend() expression: Expected a chain of function calls (e.g. x()->y()->z())`, - ); - } else { - currentFunc.parameters = currentFunc.parameters.slice(1); - extendFuncs.unshift(currentFunc); - currentFunc = valueSpecification; - } - } else { - assertTrue( - currentFunc.parameters.length === 1, - `Can't process extend() expression: Expected 1 parameter, got ${currentFunc.parameters.length}`, - ); - extendFuncs.unshift(currentFunc); - break; - } - } - return extendFuncs.map((extendFunc) => { - const colSpecs = _colSpecArrayParam(extendFunc, 0).colSpecs; +export async function _extractExtendedColumns( + funcs: V1_AppliedFunction[], + currentColumns: DataCubeColumn[], + engine: DataCubeEngine, +) { + const colSpecs = funcs.map((extendFunc) => { + // TODO: support extend() with window (OLAP), this assertion will no longer work + const _colSpecs = _colSpecArrayParam(extendFunc, 0).colSpecs; assertTrue( - colSpecs.length === 1, - `Can't process extend() expression: Expected 1 column specification, got ${colSpecs.length}`, + _colSpecs.length === 1, + `Can't process extend() expression: expected 1 column specification, got ${_colSpecs.length}`, ); - const colSpec = at(colSpecs, 0); - return { - name: colSpec.name, - type: '', // NOTE: we don't have type information for extended columns at this point - mapFn: _serializeValueSpecification( - guaranteeNonNullable( - colSpec.function1, - `Can't process extend() expression: Expected a transformation function expression`, + return at(_colSpecs, 0); + }); + + // get the types + const sourceQuery = _synthesizeMinimalSourceQuery(currentColumns); + const sequence = colSpecs.map((colSpec) => + _function(DataCubeFunction.EXTEND, [ + _cols([ + _colSpec( + colSpec.name, + guaranteeNonNullable( + colSpec.function1, + `Can't process extend() expression: expected a transformation function expression for column '${colSpec.name}'`, + ), + colSpec.function2, ), + ]), + ]), + ); + for (let i = 0; i < sequence.length; i++) { + at(sequence, i).parameters.unshift( + i === 0 ? sourceQuery : at(sequence, i - 1), + ); + } + const query = at(sequence, sequence.length - 1); + let columns: DataCubeColumn[] = []; + try { + columns = ( + await engine.getQueryRelationReturnType( + _lambda([], [query]), + new INTERNAL__DataCubeSource(), + ) + ).columns; + } catch (error) { + assertErrorThrown(error); + throw new Error( + `Can't process extend() expression: failed to retrieve type information for columns. Error: ${error.message}`, + ); + } + + return colSpecs.map((colSpec) => ({ + name: colSpec.name, + type: guaranteeNonNullable( + _findCol(columns, colSpec.name), + `Can't process extend() expression: failed to retrieve type information for column '${colSpec.name}'`, + ).type, + mapFn: _serializeValueSpecification( + guaranteeNonNullable( + colSpec.function1, + `Can't process extend() expression: expected a transformation function expression for column '${colSpec.name}'`, ), - reduceFn: colSpec.function2 - ? _serializeValueSpecification(colSpec.function2) - : undefined, - }; - }); + ), + reduceFn: colSpec.function2 + ? _serializeValueSpecification(colSpec.function2) + : undefined, + })); } export function _filter( @@ -264,7 +379,7 @@ export function _filter( ): DataCubeQuerySnapshotFilter { if (!(value instanceof V1_AppliedFunction)) { throw new Error( - `Can't process filter() expression: Expected a function expression`, + `Can't process filter() expression: expected a function expression`, ); } @@ -303,7 +418,7 @@ export function _unwrapNotFilterCondition(func: V1_AppliedFunction) { ); assertTrue( func.parameters.length === 1, - `Can't process not() function: Expected 1 parameter`, + `Can't process not() function: expected 1 parameter`, ); return _param(func, 0, V1_AppliedFunction); } @@ -315,7 +430,7 @@ function _filterCondition( ): DataCubeQuerySnapshotFilterCondition | DataCubeQuerySnapshotFilter { if (!(value instanceof V1_AppliedFunction)) { throw new UnsupportedOperationError( - `Can't process filter condition expression: Expected a function expression`, + `Can't process filter condition expression: expected a function expression`, ); } @@ -375,31 +490,6 @@ function _filterCondition( throw new Error(`Can't process filter condition: no matching operator found`); } -function _filterConditionValue( - value: V1_ValueSpecification | undefined, - column: DataCubeColumn, - columnGetter: (name: string) => DataCubeColumn, -) { - if (value instanceof V1_PrimitiveValueSpecification) { - return _operationPrimitiveValue(value); - } else if (value instanceof V1_AppliedProperty) { - const column2 = columnGetter(value.property); - if (getDataType(column.type) !== getDataType(column2.type)) { - return undefined; - } - return { - value: column2.name, - type: DataCubeOperationAdvancedValueType.COLUMN, - }; - } else if (value === undefined) { - return { - type: DataCubeOperationAdvancedValueType.VOID, - }; - } - - return undefined; -} - /** * Processes filter conditions of form: column | operator | value, e.g. * $x.Age > 5 @@ -408,40 +498,50 @@ function _filterConditionValue( * $x.Age > $x.Age2 * $x.Name == $x.Name2 */ -export function _baseFilterCondition( - expression: V1_AppliedFunction, - columnGetter: (name: string) => DataCubeColumn, +export function _filterCondition_base( + expression: V1_AppliedFunction | undefined, func: string, + columnGetter: (name: string) => DataCubeColumn, ) { - if (matchFunctionName(expression.function, func)) { - if ( - expression.parameters.length !== 2 && - expression.parameters.length !== 1 - ) { - return undefined; - } + if (!expression) { + return undefined; + } + try { + if (matchFunctionName(expression.function, func)) { + if ( + expression.parameters.length !== 2 && + expression.parameters.length !== 1 + ) { + return undefined; + } - const column = - expression.parameters[0] instanceof V1_AppliedProperty - ? columnGetter(expression.parameters[0].property) - : undefined; - if (!column) { - return undefined; - } + let column: DataCubeColumn | undefined; + if (expression.parameters[0] instanceof V1_AppliedProperty) { + column = _propertyCol(expression.parameters[0], columnGetter); + } + if (!column) { + return undefined; + } - const value = _filterConditionValue( - expression.parameters[1], - column, - columnGetter, - ); - if (!value) { - return undefined; - } + const value = _operationValue( + expression.parameters[1], + columnGetter, + (_column) => { + if (getDataType(column.type) !== getDataType(_column.type)) { + throw new Error( + `Can't process filter condition: found incompatible columns`, + ); + } + }, + ); - return { - column, - value, - }; + return { + column, + value, + }; + } + } catch { + return undefined; } return undefined; } @@ -451,58 +551,398 @@ export function _baseFilterCondition( * $x.Name->toLower() == 'abc'->toLower() * $x.Name->toLower() == $x.Name2->toLower() */ -export function _caseSensitiveBaseFilterCondition( - expression: V1_AppliedFunction, +export function _filterCondition_caseSensitive( + expression: V1_AppliedFunction | undefined, + func: string, columnGetter: (name: string) => DataCubeColumn, +) { + if (!expression) { + return undefined; + } + try { + if (matchFunctionName(expression.function, func)) { + if (expression.parameters.length !== 2) { + return undefined; + } + + const param1 = expression.parameters[0]; + if ( + !(param1 instanceof V1_AppliedFunction) || + !matchFunctionName(param1.function, DataCubeFunction.TO_LOWERCASE) + ) { + return undefined; + } + if (param1.parameters.length !== 1) { + return undefined; + } + let column: DataCubeColumn | undefined; + if (param1.parameters[0] instanceof V1_AppliedProperty) { + column = _propertyCol(param1.parameters[0], columnGetter); + } + if (!column) { + return undefined; + } + + const param2 = expression.parameters[1]; + if ( + !(param2 instanceof V1_AppliedFunction) || + !matchFunctionName(param2.function, DataCubeFunction.TO_LOWERCASE) + ) { + return undefined; + } + if (param2.parameters.length !== 1) { + return undefined; + } + const value = _operationValue( + param2.parameters[0], + columnGetter, + (_column) => { + if (getDataType(column.type) !== getDataType(_column.type)) { + throw new Error( + `Can't process filter condition: found incompatible columns`, + ); + } + }, + ); + + return { + column, + value, + }; + } + } catch { + return undefined; + } + return undefined; +} + +export function _aggCol( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + aggregateOperations: DataCubeQueryAggregateOperation[], +) { + for (const operation of aggregateOperations) { + const col = operation.buildAggregateColumnSnapshot(colSpec, columnGetter); + if (col) { + return col; + } + } + throw new Error( + `Can't process aggregate column '${colSpec.name}': no matching operator found`, + ); +} + +export function _agg_base( + colSpec: V1_ColSpec, func: string, + columnGetter: (name: string) => DataCubeColumn, ) { - if (matchFunctionName(expression.function, func)) { - if (expression.parameters.length !== 2) { - return undefined; + try { + if (colSpec.function1 && colSpec.function2) { + const mapper = _unwrapLambda(colSpec.function1); + const reducer = _unwrapLambda(colSpec.function2); + + if ( + mapper instanceof V1_AppliedProperty && + reducer instanceof V1_AppliedFunction && + reducer.parameters.length >= 1 && + matchFunctionName(reducer.function, func) + ) { + const column = _propertyCol(mapper, columnGetter); + assertTrue( + column.name === colSpec.name, + `Can't process aggregate column: column name must match mapper column name`, + ); + const variable = _param(reducer, 0, V1_Variable); + _var(variable); + + return { + column, + paramterValues: reducer.parameters + .slice(1) + .map((value) => _operationValue(value, columnGetter)), + }; + } } + } catch { + return undefined; + } + return undefined; +} - const param1 = expression.parameters[0]; - if ( - !(param1 instanceof V1_AppliedFunction) || - !matchFunctionName(param1.function, DataCubeFunction.TO_LOWERCASE) - ) { - return undefined; +export function _pivotSort( + func: V1_AppliedFunction, + pivotColumns: DataCubeColumn[], + columnGetter: (name: string) => DataCubeColumn, +) { + const sortColumns = _sort(func, columnGetter); + const groupColumns = new Set(pivotColumns.map((col) => col.name)); + const columnsToSort = new Set(pivotColumns.map((col) => col.name)); + sortColumns.forEach((col) => { + if (groupColumns.has(col.name)) { + columnsToSort.delete(col.name); + } else { + throw new Error( + `Can't process pivot() expression: sort column '${col.name}' must be a pivot column`, + ); } - if (param1.parameters.length !== 1) { - return undefined; + }); + if (columnsToSort.size !== 0) { + throw new Error( + `Can't process pivot() expression: found unsorted pivot column(s) (${Array.from( + columnsToSort.values(), + ) + .sort() + .map((col) => `'${col}'`) + .join(', ')})`, + ); + } + + _checkDuplicateColumns( + sortColumns, + (colName) => + `Can't process pivot() expression: found duplicate sort columns '${colName}'`, + ); + + return sortColumns; +} + +export function _validatePivot( + pivot: DataCubeQuerySnapshotPivot, + pivotAggColumns: DataCubeQuerySnapshotAggregateColumn[], + availableColumns: DataCubeColumn[], +) { + // check for duplicate columns + const pivotColumns = pivot.columns; + const castColumns = pivot.castColumns; + _checkDuplicateColumns( + pivotColumns, + (colName) => + `Can't process pivot() expression: found duplicate pivot columns '${colName}'`, + ); + _checkDuplicateColumns( + pivotAggColumns, + (colName) => + `Can't process pivot() expression: found duplicate aggregate columns '${colName}'`, + ); + _checkDuplicateColumns( + castColumns, + (colName) => + `Can't process pivot() expression: found duplicate cast columns '${colName}'`, + ); + + // check pivot columns are not aggregated on + pivotAggColumns.forEach((col) => { + if (_findCol(pivotColumns, col.name)) { + throw new Error( + `Can't process pivot() expression: pivot column '${col.name}' must not be aggregated on`, + ); } - const column = - param1.parameters[0] instanceof V1_AppliedProperty - ? columnGetter(param1.parameters[0].property) - : undefined; - if (!column) { - return undefined; + }); + + // check cast columns + // NOTE: we cannot and should not do strict checks here as cast columns are dependent on the data + + // check that the columns used by pivot() as group columns are present in cast columns + const pivotGroupColumns = availableColumns.filter( + (col) => + !( + _findCol(pivotColumns, col.name) ?? _findCol(pivotAggColumns, col.name) + ), + ); + pivotGroupColumns.forEach((col) => { + if (!_findCol(castColumns, col.name)) { + throw new Error( + `Can't process pivot() expression: expected pivot group column '${col.name}' in cast columns`, + ); } + }); - const param2 = expression.parameters[1]; - if ( - !(param2 instanceof V1_AppliedFunction) || - !matchFunctionName(param2.function, DataCubeFunction.TO_LOWERCASE) - ) { - return undefined; + // check that columns used in pivot() should not show up in cast columns + pivotColumns.forEach((col) => { + if (_findCol(castColumns, col.name)) { + throw new Error( + `Can't process pivot() expression: expected pivot column '${col.name}' to not present in cast columns`, + ); } - if (param2.parameters.length !== 1) { - return undefined; + }); + pivotAggColumns.forEach((col) => { + if (_findCol(castColumns, col.name)) { + throw new Error( + `Can't process pivot() expression: expected pivot aggregate column '${col.name}' to not present in cast columns`, + ); } - const value = _filterConditionValue( - param2.parameters[0], - column, - columnGetter, + }); + + // check that cast column resulted from an aggregation (usually has name of form VAL1__|__COL1) + // has a matching aggregate column (i.e. COL1) + castColumns + .filter((col) => isPivotResultColumnName(col.name)) + .forEach((col) => { + const aggColName = getPivotResultColumnBaseColumnName(col.name); + if (!_findCol(pivotAggColumns, aggColName)) { + throw new Error( + `Can't process pivot() expression: fail to match cast column '${col.name}' to a specified aggregate column`, + ); + } + }); +} + +export function _groupBySort( + func: V1_AppliedFunction, + groupByColumns: DataCubeColumn[], + columnGetter: (name: string) => DataCubeColumn, +) { + const sortColumns = _sort(func, columnGetter); + const groupColumns = new Set(groupByColumns.map((col) => col.name)); + const columnsToSort = new Set(groupByColumns.map((col) => col.name)); + let sortDirection: DataCubeQuerySortDirection | undefined = undefined; + sortColumns.forEach((col) => { + if (groupColumns.has(col.name)) { + columnsToSort.delete(col.name); + } else { + throw new Error( + `Can't process groupBy() expression: sort column '${col.name}' must be a group column`, + ); + } + + if (!sortDirection) { + sortDirection = col.direction; + } else if (col.direction !== sortDirection) { + throw new Error( + `Can't process groupBy() expression: all group columns must be sorted in the same direction`, + ); + } + }); + if (columnsToSort.size !== 0) { + throw new Error( + `Can't process groupBy() expression: found unsorted group column(s) (${Array.from( + columnsToSort.values(), + ) + .sort() + .map((col) => `'${col}'`) + .join(', ')})`, ); + } + + _checkDuplicateColumns( + sortColumns, + (colName) => + `Can't process groupBy() expression: found duplicate sort columns '${colName}'`, + ); + + return sortColumns; +} + +export function _validateGroupBy( + groupBy: DataCubeQuerySnapshotGroupBy, + groupByAggColumns: DataCubeQuerySnapshotAggregateColumn[], + pivot: DataCubeQuerySnapshotPivot | undefined, + pivotAggColumns: DataCubeQuerySnapshotAggregateColumn[], + availableColumns: DataCubeColumn[], +) { + // check for duplicate columns + const groupByColumns = groupBy.columns; + _checkDuplicateColumns( + groupByColumns, + (colName) => + `Can't process groupBy() expression: found duplicate group columns '${colName}'`, + ); + _checkDuplicateColumns( + groupByAggColumns, + (colName) => + `Can't process groupBy() expression: found duplicate aggregate columns '${colName}'`, + ); - if (!value) { - return undefined; + // check group columns are not aggregated on + groupByAggColumns.forEach((col) => { + if (_findCol(groupByColumns, col.name)) { + throw new Error( + `Can't process groupBy() expression: group column '${col.name}' must not be aggregated on`, + ); + } + }); + + // check all available columns are either grouped on or aggregatd on + availableColumns.forEach((col) => { + if ( + !( + _findCol(groupByColumns, col.name) ?? + _findCol(groupByAggColumns, col.name) + ) + ) { + throw new Error( + `Can't process groupBy() expression: column '${col.name}' is neither grouped nor aggregated on`, + ); } + }); + + // check against pivot if present + if (pivot) { + const aggCols = new Map(); + + // check if aggregation specification is consistent (i.e. same type, operator, parameterValues) + // between groupBy aggregate columns + groupByAggColumns + .filter((col) => isPivotResultColumnName(col.name)) + .forEach((col) => { + const aggColName = getPivotResultColumnBaseColumnName(col.name); + const aggCol = { + ...col, + name: aggColName, + }; + + const existingAggCol = aggCols.get(aggColName); + + if (!existingAggCol) { + aggCols.set(aggColName, aggCol); + } else if (!deepEqual(existingAggCol, aggCol)) { + throw new Error( + `Can't process groupBy() expression: found conflicting aggregation specification for column '${aggColName}'`, + ); + } + }); + + // check if pivot() aggregate columns are consistent with groupBy() aggregate columns + pivotAggColumns.forEach((col) => { + const existingAggCol = aggCols.get(col.name); + if (!existingAggCol) { + throw new Error( + `Can't process groupBy() expression: column '${col.name}' is aggregated in pivot() expression but not in groupBy() expression`, + ); + } + + if (!deepEqual(existingAggCol, col)) { + throw new Error( + `Can't process groupBy() expression: found conflicting aggregation specification for column '${col.name}'`, + ); + } + }); + } +} +export function _sort( + func: V1_AppliedFunction, + columnGetter: (name: string) => DataCubeColumn, +) { + return _param( + func, + 0, + V1_Collection, + `Can't process sort() expression: expected parameter at index 0 to be a collection`, + ).values.map((value) => { + const sortColFunc = _funcMatch(value, [ + DataCubeFunction.ASCENDING, + DataCubeFunction.DESCENDING, + ]); return { - column, - value, + ...columnGetter(_colSpecParam(sortColFunc, 0).name), + direction: matchFunctionName( + sortColFunc.function, + DataCubeFunction.ASCENDING, + ) + ? DataCubeQuerySortDirection.ASCENDING + : DataCubeQuerySortDirection.DESCENDING, }; - } - return undefined; + }); } diff --git a/packages/legend-data-cube/src/stores/core/__tests__/DataCubeQueryRoundtrip.data-cube-test.ts b/packages/legend-data-cube/src/stores/core/__tests__/DataCubeQueryRoundtrip.data-cube-test.ts index 9f05061016..6cb9f67a10 100644 --- a/packages/legend-data-cube/src/stores/core/__tests__/DataCubeQueryRoundtrip.data-cube-test.ts +++ b/packages/legend-data-cube/src/stores/core/__tests__/DataCubeQueryRoundtrip.data-cube-test.ts @@ -14,72 +14,43 @@ * limitations under the License. */ -import { ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification } from '@finos/legend-graph/test'; -import { unitTest } from '@finos/legend-shared/test'; +import { integrationTest } from '@finos/legend-shared/test'; import { describe, expect, test } from '@jest/globals'; import { validateAndBuildQuerySnapshot } from '../DataCubeQuerySnapshotBuilder.js'; -import { assertErrorThrown, type PlainObject } from '@finos/legend-shared'; +import { + assertErrorThrown, + at, + guaranteeNonNullable, + isString, +} from '@finos/legend-shared'; import { DataCubeQuery } from '../model/DataCubeQuery.js'; import { INTERNAL__DataCubeSource } from '../model/DataCubeSource.js'; -import { _deserializeValueSpecification } from '../DataCubeQueryBuilderUtils.js'; -import { DataCubeConfiguration } from '../model/DataCubeConfiguration.js'; +import { + DataCubeColumnConfiguration, + DataCubeConfiguration, +} from '../model/DataCubeConfiguration.js'; import { TEST__DataCubeEngine } from './DataCubeTestUtils.js'; import type { DataCubeQuerySnapshot, DataCubeQuerySnapshotFilterCondition, } from '../DataCubeQuerySnapshot.js'; -import { DataCubeQueryFilterOperator } from '../DataCubeQueryEngine.js'; +import { + DataCubeColumnKind, + DataCubeOperationAdvancedValueType, + DataCubeQueryAggregateOperator, + DataCubeQueryFilterOperator, + DataCubeQuerySortDirection, +} from '../DataCubeQueryEngine.js'; +import type { DataCubeColumn } from '../model/DataCubeColumn.js'; +import { + PRIMITIVE_TYPE, + type V1_ValueSpecification, +} from '@finos/legend-graph'; -type TestCase = [ - string, // name - string, // partial query - { name: string; type: string }[], // source columns - PlainObject | undefined, // configuration - string | undefined, // error - ((snapshot: DataCubeQuerySnapshot) => void) | undefined, // extra checks on snapshot -]; - -function _case( - name: string, - data: { - query: string; - columns?: string[] | undefined; - configuration?: PlainObject | undefined; - error?: string | undefined; - validator?: ((snapshot: DataCubeQuerySnapshot) => void) | undefined; - }, -): TestCase { - return [ - name, - data.query, - data.columns?.map((entry) => { - const parts = entry.split(':'); - return { - name: parts[0] as string, - type: parts[1] as string, - }; - }) ?? [], - data.configuration, - data.error, - data.validator, - ]; -} - -const FOCUSED_TESTS: string[] = [ +const FOCUSED_TESTS: unknown[] = [ // tests added here will be the only tests run ]; -function _checkFilterOperator(operator: DataCubeQueryFilterOperator) { - return (snapshot: DataCubeQuerySnapshot) => { - expect( - ( - snapshot.data.filter - ?.conditions[0] as DataCubeQuerySnapshotFilterCondition - ).operator, - ).toBe(operator); - }; -} - const cases: TestCase[] = [ // --------------------------------- LEAF-LEVEL EXTEND --------------------------------- @@ -88,34 +59,38 @@ const cases: TestCase[] = [ }), _case(`Leaf-level Extend: with complex expression`, { query: `extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['val:Integer'], }), _case(`Leaf-level Extend: multiple columns`, { query: "extend(~[name:c|$c.val->toOne() + 1])->extend(~[other:x|$x.str->toOne() + '_ext'])->extend(~[other2:x|$x.str->toOne() + '_1'])", + columns: ['val:Integer', 'str:String'], }), _case(`Leaf-level Extend: ERROR - name clash with source columns`, { query: `extend(~[name:c|$c.val->toOne() + 1])`, - columns: ['name:Integer'], - error: `Can't process leaf-level extended column 'name': another column with the same name is already registered`, + columns: ['name:Integer', 'val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: The relation contains duplicates: [name]`, + }), + _case(`Leaf-level Extend: ERROR - duplicate leaf-level extended columns`, { + query: `extend(~[name:c|$c.val->toOne() + 1])->extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: The relation contains duplicates: [name]`, }), - _case( - `Leaf-level Extend: ERROR - name clash among leaf-level extended columns`, - { - query: `extend(~[name:c|$c.val->toOne() + 1])->extend(~[name:c|$c.val->toOne() + 1])`, - columns: ['name:Integer'], - error: `Can't process leaf-level extended column 'name': another column with the same name is already registered`, - }, - ), _case( `Leaf-level Extend: ERROR - multiple columns within the same extend() expression`, { query: `extend(~[a:x|1, b:x|1])`, - error: `Can't process extend() expression: Expected 1 column specification, got 2`, + error: `Can't process extend() expression: expected 1 column specification, got 2`, }, ), _case(`Leaf-level Extend: ERROR - missing column's function expression`, { query: `extend(~[a])`, - error: `Can't process extend() expression: Expected a transformation function expression`, + error: `Can't process extend() expression: expected a transformation function expression for column 'a'`, + }), + _case(`Leaf-level Extend: ERROR - expression with compilation issue`, { + query: `extend(~[a:x|$x.val + '_123'])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: Can't find a match for function 'plus(Any[2])'`, }), // --------------------------------- FILTER --------------------------------- @@ -166,6 +141,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.NOT_EQUAL_COLUMN, ), // higher precendence than its non-negation counterpart }), + _case(`Filter: == column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Age == $x.Name))`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: == column : ERROR - incompatible columns`, { query: `filter(x|$x.Name != $x.Age)`, columns: ['Name:String', 'Age:Integer'], @@ -185,6 +165,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.NOT_EQUAL_CASE_INSENSITIVE_COLUMN, ), // higher precendence than its non-negation counterpart }), + _case(`Filter: > column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Name->toLower() == $x.Name2->toLower()))`, + columns: ['Name:String'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: == column (case-insensitive) : ERROR - incompatible columns`, { query: `filter(x|$x.Name->toLower() != $x.Name2->toLower())`, columns: ['Name:Integer', 'Name2:String'], @@ -372,6 +357,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.GREATER_THAN_COLUMN, ), }), + _case(`Filter: > column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Age > $x.Name))`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: > column : ERROR - incompatible columns`, { query: `filter(x|!($x.Age > $x.Name))`, columns: ['Age:Integer', 'Name:String'], @@ -415,6 +405,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.GREATER_THAN_OR_EQUAL_COLUMN, ), }), + _case(`Filter: >= column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Age >= $x.Name))`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: >= column : ERROR - incompatible columns`, { query: `filter(x|!($x.Age >= $x.Name))`, columns: ['Age:Integer', 'Name:String'], @@ -454,6 +449,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.LESS_THAN_COLUMN, ), }), + _case(`Filter: < column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Age < $x.Name))`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: < column : ERROR - incompatible columns`, { query: `filter(x|!($x.Age < $x.Name))`, columns: ['Age:Integer', 'Name:String'], @@ -497,6 +497,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.LESS_THAN_OR_EQUAL_COLUMN, ), }), + _case(`Filter: <= column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Age <= $x.Name))`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: <= column : ERROR - incompatible columns`, { query: `filter(x|!($x.Age <= $x.Name))`, columns: ['Age:Integer', 'Name:String'], @@ -550,6 +555,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.NOT_EQUAL_COLUMN, ), }), + _case(`Filter: != column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Name != $x.Age))`, + columns: ['Name:String'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: != column : ERROR - incompatible columns`, { query: `filter(x|!($x.Name != $x.Age))`, columns: ['Name:String', 'Age:Integer'], @@ -569,6 +579,11 @@ const cases: TestCase[] = [ DataCubeQueryFilterOperator.NOT_EQUAL_CASE_INSENSITIVE_COLUMN, ), }), + _case(`Filter: != column : ERROR - RHS column not found`, { + query: `filter(x|!($x.Name->toLower() != $x.Name2->toLower()))`, + columns: ['Name:String'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: != column (case-insensitive) : ERROR - incompatible columns`, { query: `filter(x|!($x.Name->toLower() != $x.Age->toLower()))`, columns: ['Name:String', 'Age:Integer'], @@ -670,185 +685,1263 @@ const cases: TestCase[] = [ query: `filter(x|!(($x.Age != 27) && ($x.Name == 'Michael Phelps')) && ($x.Country->startsWith('united') || ($x.Country == 'test')))`, columns: ['Age:Integer', 'Name:String', 'Country:String'], }), - _case(`Filter: with leaf-level extended column`, { - query: `extend(~[name:c|$c.val->toOne() + 1])->filter(x|$x.name != 27)`, - }), + _case(`Filter: ERROR - LHS column not found`, { + query: `filter(x|$x.Age > 1)`, + error: `Can't process filter condition: no matching operator found`, + }), + _case(`Filter: ERROR - non-standard variable name in root`, { + query: `filter(y|$x.Age > 1)`, + error: `Can't process variable 'y': expected variable name to be 'x'`, + }), + _case(`Filter: ERROR - non-standard variable name in condition LHS`, { + query: `filter(x|$y.Age == 27)`, + columns: ['Age:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), + _case(`Filter: ERROR - non-standard variable name in condition RHS`, { + query: `filter(x|$x.Age == $y.Age2)`, + columns: ['Age:Integer', 'Age2:Integer'], + error: `Can't process filter condition: no matching operator found`, + }), + _case(`Filter: ERROR - non-standard variable name within tree`, { + query: `filter(x|$x.Name->toLower()->endsWith(toLower('Phelps')) || !(($y.Age != 27) && ($x.Name == 'Michael Phelps')))`, + columns: ['Age:Integer', 'Name:String'], + error: `Can't process filter condition: no matching operator found`, + }), _case(`Filter: ERROR - bad argument: non-lambda provided`, { query: `filter('2')`, - error: `Can't process filter() expression: Expected parameter at index 0 to be a lambda expression`, + error: `Can't process filter() expression: expected parameter at index 0 to be a lambda expression`, }), _case(`Filter: ERROR - bad argument: complex lambda provided`, { query: `filter({x|let a = 1; $x.Age == 24;})`, - error: `Can't process filter() expression: Expected lambda body to have exactly 1 expression`, + error: `Can't process filter() expression: expected lambda body to have exactly 1 expression`, }), _case(`Filter: ERROR - Unsupported operator`, { query: `filter(x|$x.Age + 27 > 1)`, error: `Can't process filter condition: no matching operator found`, }), - // _case(`Filter: ERROR - simple`, { - // query: `filter(x|$x.Age.contains(27))`, - // }), // --------------------------------- SELECT --------------------------------- - _case(`Select: BASIC`, { + _case(`Select: single column`, { query: `select(~[a])`, - columns: ['a:Integer'], + columns: ['a:Integer', 'c:String'], }), - _case(`Select: BASIC`, { - query: `select(~[a])`, - columns: ['a:Integer'], + _case(`Select: multiple columns`, { + query: `select(~[a, b])`, + columns: ['a:Integer', 'b:String'], + }), + _case(`Select: ERROR - selected column not found`, { + query: `select(~[a, c])`, + columns: ['a:Integer', 'b:String'], + error: `Can't find column 'c'`, + }), + _case(`Select: ERROR - duplicate select columns`, { + query: `select(~[a, a])`, + columns: ['a:Integer', 'b:String'], + error: `Can't process select() expression: found duplicate select columns 'a'`, + }), + + // --------------------------------- AGGREGATION --------------------------------- + + _case(`Aggregation: sum()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: sum() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: sum() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: sum() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: average()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: average() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: average() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: average() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: count()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->count()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: count() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->count()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: count() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->count()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: count() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->count('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: min()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->min()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: min() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->min()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: min() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->min()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: min() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->min('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: max()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->max()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: max() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->max()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: max() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->max()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: max() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->max('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: uniqueValueOnly()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->uniqueValueOnly()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + }), + _case(`Aggregation: uniqueValueOnly() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->uniqueValueOnly()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: uniqueValueOnly() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->uniqueValueOnly('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: first()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->first()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + }), + _case(`Aggregation: first() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->first()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: first() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->first('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: last()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->last()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + }), + _case(`Aggregation: last() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->last()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: last() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->last('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: variancePopulation()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->variancePopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: variancePopulation() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->variancePopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: variancePopulation() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->variancePopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: variancePopulation() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->variancePopulation('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: varianceSample()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->varianceSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: varianceSample() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->varianceSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: varianceSample() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->varianceSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: varianceSample() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->varianceSample('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevPopulation()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevPopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: stdDevPopulation() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevPopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevPopulation() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevPopulation()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevPopulation() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevPopulation('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevSample()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Aggregation: stdDevSample() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevSample() : ERROR - incompatible column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevSample()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: stdDevSample() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->stdDevSample('asd')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: joinStrings()`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->joinStrings(',')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + }), + _case(`Aggregation: joinStrings() : ERROR - column not found`, { + query: `select(~[a])->groupBy(~[a], ~[b:x|$x.b:x|$x->joinStrings(',')])->sort([~a->ascending()])`, + columns: ['a:String', 'b:String'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Aggregation: joinStrings() : ERROR - incompatible parameters`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->joinStrings(2)])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, }), // --------------------------------- PIVOT --------------------------------- - // _case(`Validation: Bad composition pivot()`, { - // query: `pivot(~a, ~b:x|$x.a:x|$x->sum())`, - // error: `Unsupported function composition pivot() (supported composition: extend()->filter()->select()->[sort()->pivot()->cast()]->[groupBy()->sort()]->extend()->sort()->limit())`, - // }), - // _case(`Valid: Usage - Pivot: pivot()->cast()->sort()->limit()`, { - // query: `pivot(~a, ~b:x|$x.a:x|$x->sum())->cast(@meta::pure::metamodel::relation::Relation<(a:Integer)>)->sort([ascending(~a)])->limit(10)`, - // columns: ['a:Integer'], - // }), - // _case(`Valid: pivot()`, { - // query: `pivot(~a, ~b:x|$x.a:x|$x->sum())->cast(@meta::pure::metamodel::relation::Relation<(a:Integer)>)`, - // }), + _case(`Pivot: single pivot column and single aggregate column`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + }), + _case(`Pivot: single pivot column and multiple aggregate columns`, { + query: `select(~[a, b, c])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum(), c:x|$x.c:x|$x->max()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + }), + _case(`Pivot: multiple pivot columns and single aggregate column`, { + query: `select(~[a, b, c])->sort([~a->ascending(), ~c->ascending()])->pivot(~[a, c], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer', 'c:String'], + }), + _case(`Pivot: multiple pivot columns and multiple aggregate columns`, { + query: `select(~[a, b, c, d])->sort([~a->ascending(), ~d->ascending()])->pivot(~[a, d], ~[b:x|$x.b:x|$x->sum(), c:x|$x.c:x|$x->max()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + }), + _case(`Pivot: cast covering group columns`, { + query: `select(~[a, b, c, d])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(c:Integer, d:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + }), + _case(`Pivot: ERROR - pivot column not found`, { + query: `select(~[b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't find column 'a'`, + }), + _case(`Pivot: ERROR - unmatched mapped column name`, { + query: `select(~[a, b, c])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.c:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Pivot: ERROR - non-standard variable name in mapper`, { + query: `select(~[a])->sort([~a->ascending()])->pivot(~[a], ~[b:y|$y.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Pivot: ERROR - non-standard variable name in reducer`, { + query: `select(~[a])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:y|$y->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`Pivot: ERROR - duplicate group columns`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a, a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: found duplicate pivot columns 'a'`, + }), + _case(`Pivot: ERROR - duplicate aggregate columns`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum(), b:x|$x.b:x|$x->max()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: found duplicate aggregate columns 'b'`, + }), + _case(`Pivot: ERROR - duplicate sort columns`, { + query: `select(~[a, b])->sort([~a->ascending(), ~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: found duplicate sort columns 'a'`, + }), + _case(`Pivot: ERROR - sorted non-group columns`, { + query: `select(~[a, b])->sort([~b->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: sort column 'b' must be a pivot column`, + }), + _case(`Pivot: ERROR - unsorted group columns`, { + query: `select(~[a, b])->sort([])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: found unsorted pivot column(s) ('a')`, + }), + _case(`Pivot: ERROR - aggregated on pivot column`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum(), a:x|$x.a:x|$x->uniqueValueOnly()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: pivot column 'a' must not be aggregated on`, + }), _case(`Pivot: ERROR - casting used without dynamic function pivot()`, { query: `cast(@meta::pure::metamodel::relation::Relation<(a:Integer)>)`, - error: `Can't process expression: Unsupported function composition cast() (supported composition: extend()->filter()->select()->[sort()->pivot()->cast()]->[groupBy()->sort()]->extend()->sort()->limit())`, + error: `Can't process expression: unsupported function composition cast() (supported composition: extend()->filter()->select()->[sort()->pivot()->cast()]->[groupBy()->sort()]->extend()->sort()->limit())`, + }), + _case(`Pivot: ERROR - pivot group column not found in cast columns`, { + query: `select(~[a, b, c, d])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + error: `Can't process pivot() expression: expected pivot group column 'c' in cast columns`, + }), + _case(`Pivot: ERROR - pivot column found in cast columns`, { + query: `select(~[a, b, c, d])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(a:String, c:Integer, d:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + error: `Can't process pivot() expression: expected pivot column 'a' to not present in cast columns`, + }), + _case(`Pivot: ERROR - pivot aggregate column found in cast columns`, { + query: `select(~[a, b, c, d])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(b:String, c:Integer, d:String)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + error: `Can't process pivot() expression: expected pivot aggregate column 'b' to not present in cast columns`, + }), + _case(`Pivot: ERROR - aggregate columns mismatch implied by cast columns`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<('val1__|__c':String)>)`, + columns: ['a:String', 'b:Integer'], + error: `Can't process pivot() expression: fail to match cast column 'val1__|__c' to a specified aggregate column`, }), // --------------------------------- GROUP BY --------------------------------- - _case(`GroupBy: BASIC`, { + _case(`GroupBy: single group column and single aggregate column`, { query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, columns: ['a:String', 'b:Integer'], }), + _case(`GroupBy: single group column and multiple aggregate columns`, { + query: `select(~[a, b, c])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum(), c:x|$x.c:x|$x->max()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + }), + _case(`GroupBy: multiple group columns and single aggregate column`, { + query: `select(~[a, b, c])->groupBy(~[a, c], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending(), ~c->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:String'], + }), + _case(`GroupBy: multiple group columns and multiple aggregate columns`, { + query: `select(~[a, b, c, d])->groupBy(~[a, d], ~[b:x|$x.b:x|$x->sum(), c:x|$x.c:x|$x->max()])->sort([~a->ascending(), ~d->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:Integer', 'd:String'], + }), + _case(`GroupBy: following pivot() expression`, { + query: `select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, a:Integer, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->sum(), a:x|$x.a:x|$x->sum()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + }), + _case(`GroupBy: ERROR - group column not found`, { + query: `select(~[b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't find column 'a'`, + }), + _case(`GroupBy: ERROR - unmatched mapped column name`, { + query: `select(~[a, b, c])->groupBy(~[a], ~[b:x|$x.c:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`GroupBy: ERROR - non-standard variable name in mapper`, { + query: `select(~[a])->groupBy(~[a], ~[b:y|$y.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`GroupBy: ERROR - non-standard variable name in reducer`, { + query: `select(~[a])->groupBy(~[a, a], ~[b:x|$x.b:y|$y->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process aggregate column 'b': no matching operator found`, + }), + _case(`GroupBy: ERROR - duplicate group columns`, { + query: `select(~[a, b])->groupBy(~[a, a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: found duplicate group columns 'a'`, + }), + _case(`GroupBy: ERROR - duplicate aggregate columns`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum(), b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: found duplicate aggregate columns 'b'`, + }), + _case(`GroupBy: ERROR - duplicate sort columns`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending(), ~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: found duplicate sort columns 'a'`, + }), + _case(`GroupBy: ERROR - sorted non-group columns`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~b->descending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: sort column 'b' must be a group column`, + }), + _case(`GroupBy: ERROR - mixed sort directions`, { + query: `select(~[a, b, c])->groupBy(~[a, c], ~[b:x|$x.b:x|$x->sum()])->sort([~a->descending(), ~c->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:String'], + error: `Can't process groupBy() expression: all group columns must be sorted in the same direction`, + }), + _case(`GroupBy: ERROR - unsorted group columns`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: found unsorted group column(s) ('a')`, + }), + _case(`GroupBy: ERROR - aggregated on group column`, { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum(), a:x|$x.a:x|$x->uniqueValueOnly()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + error: `Can't process groupBy() expression: group column 'a' must not be aggregated on`, + }), + _case(`GroupBy: ERROR - column not grouped nor aggregated`, { + query: `select(~[a, b, c])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + error: `Can't process groupBy() expression: column 'c' is neither grouped nor aggregated on`, + }), + _case( + `GroupBy: ERROR - conflicting aggregation specifications within groupBy() expression`, + { + query: `select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, a:Integer, 'val1__|__b':Integer, 'val2__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->sum(), 'val2__|__b':x|$x.'val2__|__b':x|$x->max(), a:x|$x.a:x|$x->sum()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + error: `Can't process groupBy() expression: found conflicting aggregation specification for column 'b'`, + }, + ), + _case( + `GroupBy: ERROR - conflicting aggregation specifications between groupBy() and pivot() expression`, + { + query: `select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, a:Integer, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->max(), a:x|$x.a:x|$x->sum()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + error: `Can't process groupBy() expression: found conflicting aggregation specification for column 'b'`, + }, + ), + _case( + `GroupBy: ERROR - column aggregated in pivot() but not in groupBy() expression`, + { + query: `select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum(), a:x|$x.a:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->sum()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + error: `Can't process groupBy() expression: column 'a' is aggregated in pivot() expression but not in groupBy() expression`, + }, + ), + _case( + `GroupBy: ERROR - column pivoted but aggregated in groupBy() expression`, + { + query: `select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum(), a:x|$x.a:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->sum()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + error: `Can't process groupBy() expression: column 'a' is aggregated in pivot() expression but not in groupBy() expression`, + }, + ), // --------------------------------- GROUP-LEVEL EXTEND --------------------------------- + _case(`Group-level Extend: with simple expression`, { + query: `select(~[b])->extend(~[a:x|1])`, + columns: ['b:Integer'], + }), + _case(`Group-level Extend: with complex expression`, { + query: `select(~[val])->extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['val:Integer'], + }), + _case(`Group-level Extend: multiple columns`, { + query: + "select(~[val, str])->extend(~[name:c|$c.val->toOne() + 1])->extend(~[other:x|$x.str->toOne() + '_ext'])->extend(~[other2:x|$x.str->toOne() + '_1'])", + columns: ['val:Integer', 'str:String'], + }), + _case(`Group-level Extend: ERROR - name clash with source columns`, { + query: `select(~[name, val])->extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['name:Integer', 'val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: The relation contains duplicates: [name]`, + }), + _case( + `Group-level Extend: ERROR - name clash with leaf-level extended columns`, + { + query: `extend(~[name:c|$c.val->toOne() + 1])->select(~[name, val])->extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: The relation contains duplicates: [name]`, + }, + ), + _case(`Group-level Extend: ERROR - duplicate group-level extended columns`, { + query: `select(~[val])->extend(~[name:c|$c.val->toOne() + 1])->extend(~[name:c|$c.val->toOne() + 1])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: The relation contains duplicates: [name]`, + }), + _case( + `Group-level Extend: ERROR - multiple columns within the same extend() expression`, + { + query: `select(~[val])->extend(~[a:x|1, b:x|1])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: expected 1 column specification, got 2`, + }, + ), + _case(`Group-level Extend: ERROR - missing column's function expression`, { + query: `select(~[val])->extend(~[a])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: expected a transformation function expression for column 'a'`, + }), + _case(`Group-level Extend: ERROR - expression with compilation issue`, { + query: `select(~[val])->extend(~[a:x|$x.val + '_123'])`, + columns: ['val:Integer'], + error: `Can't process extend() expression: failed to retrieve type information for columns. Error: Can't find a match for function 'plus(Any[2])'`, + }), + // --------------------------------- SORT --------------------------------- - _case(`Sort: BASIC`, { - query: `sort([~a->ascending()])`, + _case(`Sort: single column ascending`, { + query: `select(~[a])->sort([~a->ascending()])`, columns: ['a:Integer'], }), - _case(`Sort: multiple columns`, { - query: `sort([~a->ascending(), ~b->descending()])`, + _case(`Sort: single column descending`, { + query: `select(~[a])->sort([~a->descending()])`, + columns: ['a:String'], + }), + _case(`Sort: multiple columns ascending`, { + query: `select(~[a, b])->sort([~a->ascending(), ~b->ascending()])`, + columns: ['a:Integer', 'b:Integer'], + }), + _case(`Sort: multiple columns descending`, { + query: `select(~[a, b])->sort([~a->descending(), ~b->descending()])`, + columns: ['a:Integer', 'b:Integer'], + }), + _case(`Sort: multiple columns mixed directions`, { + query: `select(~[a, b])->sort([~a->ascending(), ~b->descending()])`, columns: ['a:Integer', 'b:Integer'], }), _case(`Sort: ERROR - bad argument: non-collection provided`, { - query: `sort(~a->something())`, + query: `select(~[a])->sort(~a->something())`, columns: ['a:Integer'], - error: `Can't process sort() expression: Found unexpected type for parameter at index 0`, + error: `Can't process sort() expression: expected parameter at index 0 to be a collection`, }), _case(`Sort: ERROR - unsupported function`, { - query: `sort([~a->something()])`, + query: `select(~[a])->sort([~a->something()])`, columns: ['a:Integer'], - error: `Can't process function: Expected function to be one of [ascending, descending]`, + error: `Can't process function: expected function to be one of [ascending, descending]`, + }), + _case(`Sort: ERROR - column not found`, { + query: `select(~[a])->sort([~b->ascending()])`, + columns: ['a:Integer'], + error: `Can't find column 'b'`, + }), + _case(`Sort: ERROR - duplicate columns`, { + query: `select(~[a])->sort([~a->ascending(), ~a->ascending()])`, + columns: ['a:Integer'], + error: `Can't process sort() expression: found duplicate sort columns 'a'`, }), // --------------------------------- LIMIT --------------------------------- - _case(`Limit: BASIC`, { + _case(`Limit: with integer number`, { query: `limit(10)`, }), + _case(`Limit: ERROR - bad argument: decimal number provided`, { + query: `limit(15.5)`, + error: `Can't process limit() expression: expected limit to be a non-negative integer value`, + }), + _case(`Limit: ERROR - bad argument: non-negative number provided`, { + query: `limit(-10)`, + error: `Can't process limit() expression: expected limit to be a non-negative integer value`, + }), _case(`Limit: ERROR - bad argument: non-integer provided`, { query: `limit('asd')`, - error: `Can't process limit() expression: Expected parameter at index 0 to be an integer instance value`, - }), - - // --------------------------------- COMPOSITION --------------------------------- - - // _case(`Composition: extend()->filter()->sort()->limit()`, { - // query: `extend(~[a:x|1])->filter(x|$x.a==1)->sort([ascending(~a)])->limit(10)`, - // columns: ['b:Integer'], - // }), - // _case(`Composition: extend()->filter()->select()->sort()->limit()`, { - // query: `extend(~[a:x|1])->filter(x|$x.a==1)->select(~[a])->sort([ascending(~a)])->limit(10)`, - // columns: ['b:Integer'], - // }), - // _case(`Composition: extend()->groupBy()->extend()->sort()->limit()`, { - // query: `extend(~[a:x|1])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([ascending(~a)])->extend(~[b:x|2])->limit(10)`, - // }), - // _case(`Composition: extend()->filter()->groupBy()->extend()->sort()->limit()`, { - // query: `extend(~[a:x|1])->filter(x|$x.a==1)->groupBy(~[a], ~b:x|$x.b:x|$x->sum())->sort([ascending(~a)])->extend(~[c:x|2])->limit(10)`, - // columns: ['b:Integer'], - // }), - // _case(`Composition: extend()->filter()->groupBy()->sort()->limit()`, { - // query: `extend(~[a:x|1])->filter(x|$x.a==1)->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([ascending(~a)])->limit(10)`, - // columns: ['b:Integer'], - // }), - - // --------------------------------- VALIDATION --------------------------------- - - _case(`Validation: ERROR - not a function expression`, { + error: `Can't process limit() expression: expected limit to be a non-negative integer value`, + }), + + // --------------------------------- CONFIGURATION --------------------------------- + + _case(`Configuration: generated configuration`, { + query: `select(~[a, b])`, + columns: ['a:String', 'b:Integer', 'c:Date'], + // the assertions make sure the generated configurations have correct default values + // computed based on query processing metadata + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + + expect(config.treeColumnSortDirection).toEqual( + DataCubeQuerySortDirection.ASCENDING, + ); + + const colA = config.columns[0]; + expect(colA?.name).toEqual('a'); + expect(colA?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colA?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colA?.isSelected).toEqual(true); + expect(colA?.excludedFromPivot).toEqual(true); + expect(colA?.pivotSortDirection).toBeUndefined(); + + const colB = config.columns[1]; + expect(colB?.name).toEqual('b'); + expect(colB?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colB?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.SUM, + ); + expect(colB?.isSelected).toEqual(true); + expect(colB?.excludedFromPivot).toEqual(false); + expect(colB?.pivotSortDirection).toBeUndefined(); + + const colC = config.columns[2]; + expect(colC?.name).toEqual('c'); + expect(colC?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colC?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colC?.isSelected).toEqual(false); + expect(colC?.excludedFromPivot).toEqual(true); + expect(colC?.pivotSortDirection).toBeUndefined(); + }, + }), + _case(`Configuration: generated configuration with extended columns`, { + query: `extend(~[d:x|1])->extend(~[f:x|1])->select(~[c, d, a])->extend(~[e:x|'asd'])`, + columns: ['a:String', 'b:Integer', 'c:Date'], + // the assertions make sure the generated configurations have columns arranged + // in the right order (i.e. accounted for extended columns and respected selection order) + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + expect(config.columns[0]?.name).toEqual('c'); + expect(config.columns[1]?.name).toEqual('d'); + expect(config.columns[2]?.name).toEqual('a'); + expect(config.columns[3]?.name).toEqual('e'); + expect(config.columns[4]?.name).toEqual('b'); + expect(config.columns[5]?.name).toEqual('f'); + + // do some basic checks on the extended columns + const colD = guaranteeNonNullable(config.columns[1]); + expect(colD.type).toEqual(PRIMITIVE_TYPE.INTEGER); + expect(colD.kind).toEqual(DataCubeColumnKind.MEASURE); + + const colE = guaranteeNonNullable(config.columns[3]); + expect(colE.type).toEqual(PRIMITIVE_TYPE.STRING); + expect(colE.kind).toEqual(DataCubeColumnKind.DIMENSION); + + const colF = guaranteeNonNullable(config.columns[5]); + expect(colF.type).toEqual(PRIMITIVE_TYPE.INTEGER); + expect(colF.kind).toEqual(DataCubeColumnKind.MEASURE); + }, + }), + _case(`Configuration: without aggregation`, { + query: `select(~[a, b, c, d, e])`, + columns: ['a:String', 'b:String', 'c:Integer', 'd:Float', 'e:Date'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + + config.treeColumnSortDirection = DataCubeQuerySortDirection.DESCENDING; + + const colA = guaranteeNonNullable(config.columns[0]); + colA.blur = true; + + const colB = guaranteeNonNullable(config.columns[1]); + colB.kind = DataCubeColumnKind.MEASURE; + colB.aggregateOperator = DataCubeQueryAggregateOperator.LAST; + + const colC = guaranteeNonNullable(config.columns[2]); + colC.displayName = 'Column C'; + + const colD = guaranteeNonNullable(config.columns[3]); + colD.kind = DataCubeColumnKind.DIMENSION; + colD.aggregateOperator = DataCubeQueryAggregateOperator.UNIQUE; + + const colE = guaranteeNonNullable(config.columns[4]); + colE.fontSize = 100; + + return config; + }, + // when no aggregation is present, a few column properties are freely set + // i.e. they are not checked, since there is no query processing metadata + // to verify against + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + + expect(config.treeColumnSortDirection).toEqual( + DataCubeQuerySortDirection.DESCENDING, + ); + + const colA = config.columns[0]; + expect(colA?.name).toEqual('a'); + expect(colA?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colA?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colA?.excludedFromPivot).toEqual(true); + expect(colA?.pivotSortDirection).toBeUndefined(); + expect(colA?.blur).toEqual(true); + + const colB = config.columns[1]; + expect(colB?.name).toEqual('b'); + expect(colB?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colB?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.LAST, + ); + expect(colB?.excludedFromPivot).toEqual(true); + expect(colB?.pivotSortDirection).toBeUndefined(); + + const colC = config.columns[2]; + expect(colC?.name).toEqual('c'); + expect(colC?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colC?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.SUM, + ); + expect(colC?.excludedFromPivot).toEqual(false); + expect(colC?.pivotSortDirection).toBeUndefined(); + expect(colC?.displayName).toEqual('Column C'); + + const colD = config.columns[3]; + expect(colD?.name).toEqual('d'); + expect(colD?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colD?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colD?.excludedFromPivot).toEqual(false); + expect(colD?.pivotSortDirection).toBeUndefined(); + + const colE = config.columns[4]; + expect(colE?.name).toEqual('e'); + expect(colE?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colE?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colE?.excludedFromPivot).toEqual(true); + expect(colE?.pivotSortDirection).toBeUndefined(); + expect(colE?.fontSize).toEqual(100); + }, + }), + _case(`Configuration: with groupBy() expression`, { + query: `select(~[a, b, c, d])->groupBy(~[d, c, b], ~[a:x|$x.a:x|$x->average()])->sort([~d->descending(), ~c->descending(), ~b->descending()])`, + columns: ['a:Integer', 'b:Integer', 'c:String', 'd:Date'], + configurationBuilder: _generateDefaultConfiguration, + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + + expect(config.treeColumnSortDirection).toEqual( + DataCubeQuerySortDirection.DESCENDING, + ); + + const colA = config.columns[0]; + expect(colA?.name).toEqual('a'); + expect(colA?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colA?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.AVERAGE, + ); + + const colB = config.columns[1]; + expect(colB?.name).toEqual('b'); + expect(colB?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colB?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.SUM, + ); + + const colC = config.columns[2]; + expect(colC?.name).toEqual('c'); + expect(colC?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colC?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + + const colD = config.columns[3]; + expect(colD?.name).toEqual('d'); + expect(colD?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colD?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + }, + }), + _case(`Configuration: with pivot() expression`, { + query: `select(~[a, b, c, d])->sort([~c->ascending(), ~d->descending()])->pivot(~[c, d], ~[b:x|$x.b:x|$x->count()])->cast(@meta::pure::metamodel::relation::Relation<(a:Integer, 'val1__|__val2__|__b':Integer)>)`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + configurationBuilder: _generateDefaultConfiguration, + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + + const colA = config.columns[0]; + expect(colA?.name).toEqual('a'); + expect(colA?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colA?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.SUM, + ); + expect(colA?.excludedFromPivot).toEqual(true); + expect(colA?.pivotSortDirection).toBeUndefined(); + + const colB = config.columns[1]; + expect(colB?.name).toEqual('b'); + expect(colB?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colB?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.COUNT, + ); + expect(colB?.excludedFromPivot).toEqual(false); + expect(colB?.pivotSortDirection).toBeUndefined(); + + const colC = config.columns[2]; + expect(colC?.name).toEqual('c'); + expect(colC?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colC?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colC?.excludedFromPivot).toEqual(true); + expect(colC?.pivotSortDirection).toEqual( + DataCubeQuerySortDirection.ASCENDING, + ); + + const colD = config.columns[3]; + expect(colD?.name).toEqual('d'); + expect(colD?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colD?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colD?.excludedFromPivot).toEqual(true); + expect(colD?.pivotSortDirection).toEqual( + DataCubeQuerySortDirection.DESCENDING, + ); + }, + }), + _case(`Configuration: with both pivot() and groupBy() expression`, { + query: `select(~[a, b, c, d])->sort([~c->descending()])->pivot(~[c], ~[b:x|$x.b:x|$x->average()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, a:Integer, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->average(), a:x|$x.a:x|$x->count()])->sort([~d->ascending()])`, + columns: ['a:Integer', 'c:String', 'b:Integer', 'd:String'], + configurationBuilder: _generateDefaultConfiguration, + validator: (snapshot) => { + const config = DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + + const colA = config.columns[0]; + expect(colA?.name).toEqual('a'); + expect(colA?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colA?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.COUNT, + ); + expect(colA?.excludedFromPivot).toEqual(true); + expect(colA?.pivotSortDirection).toBeUndefined(); + + const colB = config.columns[1]; + expect(colB?.name).toEqual('b'); + expect(colB?.kind).toEqual(DataCubeColumnKind.MEASURE); + expect(colB?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.AVERAGE, + ); + expect(colB?.excludedFromPivot).toEqual(false); + expect(colB?.pivotSortDirection).toBeUndefined(); + + const colC = config.columns[2]; + expect(colC?.name).toEqual('c'); + expect(colC?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colC?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colC?.excludedFromPivot).toEqual(true); + expect(colC?.pivotSortDirection).toEqual( + DataCubeQuerySortDirection.DESCENDING, + ); + + const colD = config.columns[3]; + expect(colD?.name).toEqual('d'); + expect(colD?.kind).toEqual(DataCubeColumnKind.DIMENSION); + expect(colD?.aggregateOperator).toEqual( + DataCubeQueryAggregateOperator.UNIQUE, + ); + expect(colD?.excludedFromPivot).toEqual(true); + expect(colD?.pivotSortDirection).toBeUndefined(); + }, + }), + + _case(`Configuration: ERROR - tree column sort direction mismatch`, { + query: `select(~[a, b])->groupBy(~[b], ~[a:x|$x.a:x|$x->average()])->sort([~b->ascending()])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + config.treeColumnSortDirection = DataCubeQuerySortDirection.DESCENDING; + return config; + }, + error: `Can't process configuration: tree column sort direction mismatch (expected: 'ascending', found: 'descending')`, + }), + _case(`Configuration: ERROR - duplicate columns`, { + query: `select(~[a, b])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + config.columns.push(at(config.columns, 1)); + return config; + }, + error: `Can't process configuration: found duplicate columns 'b'`, + }), + _case(`Configuration: ERROR - found extra column`, { + query: `select(~[a, b])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + const extraCol = DataCubeColumnConfiguration.serialization.fromJson( + DataCubeColumnConfiguration.serialization.toJson(at(config.columns, 1)), + ); + extraCol.name = 'c'; + config.columns.push(extraCol); + return config; + }, + error: `Can't process configuration: found extra column 'c'`, + }), + _case(`Configuration: ERROR - found missing column`, { + query: `select(~[a, b])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + config.columns.splice(0, 1); + return config; + }, + error: `Can't process configuration: missing column 'a'`, + }), + _case(`Configuration: ERROR - column ordering mismatch`, { + query: `extend(~[d:x|1])->extend(~[f:x|1])->select(~[c, d, a])->extend(~[e:x|'asd'])`, + columns: ['a:String', 'b:Integer', 'c:Date'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + const [col] = config.columns.splice(0, 1); + config.columns.push(guaranteeNonNullable(col)); + return config; + }, + error: `Can't process configuration: column ordering mismatch at index 0 (expected: 'c', found: 'd)', expected ordering: c, d, a, e, b, f`, + }), + _case(`Configuration: ERROR - column type mismatch`, { + query: `select(~[a, b])->groupBy(~[b], ~[a:x|$x.a:x|$x->average()])->sort([~b->ascending()])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 0).type = PRIMITIVE_TYPE.STRING; + return config; + }, + error: `Can't process configuration: type mismatch for column 'a' (expected: 'Integer', found: 'String')`, + }), + _case(`Configuration: ERROR - column selection mismatch`, { + query: `select(~[a, b])->groupBy(~[b], ~[a:x|$x.a:x|$x->average()])->sort([~b->ascending()])`, + columns: ['a:Integer', 'b:String'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 0).isSelected = false; + return config; + }, + error: `Can't process configuration: selection mismatch for column 'a' (expected: 'true', found: 'false')`, + }), + _case( + `Configuration: ERROR - column kind mismatch (aggregate column in pivot() expression)`, + { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).kind = DataCubeColumnKind.DIMENSION; + return config; + }, + error: `Can't process configuration: kind mismatch for column 'b' (expected: 'measure', found: 'dimension')`, + }, + ), + _case( + `Configuration: ERROR - column kind mismatch (aggregate column in groupBy() expression)`, + { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).kind = DataCubeColumnKind.DIMENSION; + return config; + }, + error: `Can't process configuration: kind mismatch for column 'b' (expected: 'measure', found: 'dimension')`, + }, + ), + _case(`Configuration: ERROR - column kind mismatch (pivot column)`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 0).kind = DataCubeColumnKind.MEASURE; + return config; + }, + error: `Can't process configuration: kind mismatch for column 'a' (expected: 'dimension', found: 'measure')`, + }), + _case( + `Configuration: ERROR - column kind mismatch (group column in groupBy() expression)`, + { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 0).kind = DataCubeColumnKind.MEASURE; + return config; + }, + error: `Can't process configuration: kind mismatch for column 'a' (expected: 'dimension', found: 'measure')`, + }, + ), + _case( + `Configuration: ERROR - column aggregation operator mismatch (aggregate column in pivot() expression)`, + { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->average()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).aggregateOperator = + DataCubeQueryAggregateOperator.SUM; + return config; + }, + error: `Can't process configuration: aggregation operator mismatch for column 'b' (expected: 'avg', found: 'sum')`, + }, + ), + _case( + `Configuration: ERROR - column aggregation operator mismatch (aggregate column in groupBy() expression)`, + { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).aggregateOperator = + DataCubeQueryAggregateOperator.SUM; + return config; + }, + error: `Can't process configuration: aggregation operator mismatch for column 'b' (expected: 'avg', found: 'sum')`, + }, + ), + _case( + `Configuration: ERROR - incompatible column aggregation parameter values (aggregate column in pivot() expression)`, + { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->average()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).aggregationParameters = [ + { + type: DataCubeOperationAdvancedValueType.VOID, + }, + ]; + return config; + }, + error: `Can't process configuration: incompatible aggregation parameter values for column 'b' (operator: 'avg')`, + }, + ), + _case( + `Configuration: ERROR - incompatible column aggregation parameter values (aggregate column in groupBy() expression)`, + { + query: `select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->average()])->sort([~a->ascending()])`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 1).aggregationParameters = [ + { + type: DataCubeOperationAdvancedValueType.VOID, + }, + ]; + return config; + }, + error: `Can't process configuration: incompatible aggregation parameter values for column 'b' (operator: 'avg')`, + }, + ), + _case(`Configuration: ERROR - column pivot exclusion mismatch`, { + query: `select(~[a, b, c])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->average()])->cast(@meta::pure::metamodel::relation::Relation<(c:Integer)>)`, + columns: ['a:String', 'b:Integer', 'c:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 2).excludedFromPivot = false; + return config; + }, + error: `Can't process configuration: pivot exclusion mismatch for column 'c' (expected: 'true', found: 'false')`, + }), + _case(`Configuration: ERROR - column pivot sort direction mismatch`, { + query: `select(~[a, b])->sort([~a->ascending()])->pivot(~[a], ~[b:x|$x.b:x|$x->average()])->cast(@meta::pure::metamodel::relation::Relation<(dummy:String)>)`, + columns: ['a:String', 'b:Integer'], + configurationBuilder: async (query, columns) => { + const config = await _generateDefaultConfiguration(query, columns); + at(config.columns, 0).pivotSortDirection = undefined; + return config; + }, + error: `Can't process configuration: pivot sort direction mismatch for column 'a' (expected: 'ascending', found: 'none')`, + }), + + // --------------------------------- GENERAL --------------------------------- + + _case(`GENERAL: Composition - extend()->filter()->sort()->limit()`, { + query: `extend(~[a:x|1])->filter(x|$x.a == 1)->select(~[a])->sort([~a->ascending()])->limit(10)`, + }), + _case( + `GENERAL: Composition - extend()->filter()->select()->sort()->limit()`, + { + query: `extend(~[a:x|1])->filter(x|$x.a == 1)->select(~[a])->sort([~a->ascending()])->limit(10)`, + }, + ), + _case( + `GENERAL: Composition - extend()->groupBy()->extend()->sort()->limit()`, + { + query: `extend(~[a:x|1])->select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])->extend(~[c:x|2])->limit(10)`, + columns: ['b:Integer'], + }, + ), + _case( + `GENERAL: Composition - extend()->groupBy()->extend()->extend()->sort()->limit()`, + { + query: `extend(~[a:x|1])->select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])->extend(~[c:x|2])->extend(~[d:x|2])->limit(10)`, + columns: ['b:Integer'], + }, + ), + _case( + `GENERAL: Composition - extend()->filter()->groupBy()->extend()->sort()->limit()`, + { + query: `extend(~[a:x|1])->filter(x|$x.a == 1)->select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])->extend(~[c:x|2])->limit(10)`, + columns: ['b:Integer'], + }, + ), + _case( + `GENERAL: Composition - extend()->filter()->groupBy()->sort()->limit()`, + { + query: `extend(~[a:x|1])->filter(x|$x.a == 1)->select(~[a, b])->groupBy(~[a], ~[b:x|$x.b:x|$x->sum()])->sort([~a->ascending()])->limit(10)`, + columns: ['b:Integer'], + }, + ), + _case( + `GENERAL: Composition - extend()->filter()->sort()->pivot()->cast()->groupBy()->sort()->sort()->limit()`, + { + query: `extend(~[a:x|1])->filter(x|$x.a == 1)->select(~[a, b, c, d])->sort([~c->ascending()])->pivot(~[c], ~[b:x|$x.b:x|$x->sum()])->cast(@meta::pure::metamodel::relation::Relation<(d:String, a:Integer, 'val1__|__b':Integer)>)->groupBy(~[d], ~['val1__|__b':x|$x.'val1__|__b':x|$x->sum(), a:x|$x.a:x|$x->sum()])->sort([~d->ascending()])->limit(10)`, + columns: ['c:String', 'b:Integer', 'd:String'], + }, + ), + + // validations + _case(`GENERAL: ERROR - not a function expression`, { query: `2`, - error: `Can't process expression: Expected a function expression`, + error: `Can't process expression: expected a function expression`, }), - _case(`Validation: ERROR - not a chain of function calls`, { + _case(`GENERAL: ERROR - not a chain of function calls`, { query: `select(~[a, b], 'something')`, columns: ['a:Integer', 'b:Integer'], - error: `Can't process expression: Expected a sequence of function calls (e.g. x()->y()->z())`, + error: `Can't process expression: expected a sequence of function calls (e.g. x()->y()->z())`, }), - _case(`Validation: ERROR - unsupported function`, { + _case(`GENERAL: ERROR - unsupported function`, { query: `sort([~asd->ascending()])->something()`, - error: `Can't process expression: Found unsupported function something()`, + error: `Can't process expression: found unsupported function something()`, }), - _case(`Validation: ERROR - wrong number of paramters provided`, { + _case(`GENERAL: ERROR - wrong number of paramters provided`, { query: `select(~[a, b], 2, 'asd')`, columns: ['a:Integer', 'b:Integer'], - error: `Can't process select() expression: Expected at most 2 parameters provided, got 3`, + error: `Can't process select() expression: expected at most 2 parameters provided, got 3`, }), - _case(`Validation: ERROR - bad composition: select()->filter()`, { + _case(`GENERAL: ERROR - bad composition: select()->filter()`, { query: `select(~a)->filter(x|$x.a==1)`, columns: ['a:Integer'], - error: `Can't process expression: Unsupported function composition select()->filter() (supported composition: extend()->filter()->select()->[sort()->pivot()->cast()]->[groupBy()->sort()]->extend()->sort()->limit())`, + error: `Can't process expression: unsupported function composition select()->filter() (supported composition: extend()->filter()->select()->[sort()->pivot()->cast()]->[groupBy()->sort()]->extend()->sort()->limit())`, }), - _case(`Validation: ERROR - name clash among source columns`, { + _case(`GENERAL: ERROR - duplicate source columns`, { query: `select(~[a, b])`, columns: ['a:Integer', 'a:Integer', 'b:Integer'], - error: `Can't process source column 'a': another column with the same name is already registered`, + error: `Can't process source: found duplicate source columns 'a'`, }), - // TODO: vaidation against configuration ]; -describe(unitTest('Analyze and build base snapshot'), () => { - test.each(cases)( +describe(integrationTest('Roundtrip query processing'), () => { + // make sure no tests are accidentally skipped during development + if (FOCUSED_TESTS.length) { + test('DEV: No test should be skipped!', () => { + throw new Error( + `No tests should be skipped! Remove any focus tests specified during development.`, + ); + }); + } + + test.each( + cases.filter((c) => { + if ( + !FOCUSED_TESTS.length || + FOCUSED_TESTS.some((pattern) => { + if (isString(pattern)) { + return pattern.trim() === c[0].trim(); + } else if (pattern instanceof RegExp) { + return c[0].match(pattern); + } + return false; + }) + ) { + return true; + } + return false; + }), + )( '%s', async ( testName: TestCase[0], code: TestCase[1], columns: TestCase[2], - configuration: TestCase[3], + configurationBuilder: TestCase[3], error: TestCase[4], validator: TestCase[5], ) => { - if (FOCUSED_TESTS.length && !FOCUSED_TESTS.includes(testName)) { - return; - } - const engine = new TEST__DataCubeEngine(); - const partialQuery = _deserializeValueSpecification( - await ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification(code), - ); + const query = await engine.parseValueSpecification(code); const baseQuery = new DataCubeQuery(); - baseQuery.configuration = configuration - ? DataCubeConfiguration.serialization.fromJson(configuration) - : undefined; const source = new INTERNAL__DataCubeSource(); source.columns = columns; - + baseQuery.configuration = configurationBuilder + ? await configurationBuilder(query, columns) + : undefined; let snapshot: DataCubeQuerySnapshot | undefined; try { - snapshot = validateAndBuildQuerySnapshot( - partialQuery, + snapshot = await validateAndBuildQuerySnapshot( + query, source, baseQuery, - engine.filterOperations, - engine.aggregateOperations, + engine, ); } catch (err) { assertErrorThrown(err); @@ -863,3 +1956,99 @@ describe(unitTest('Analyze and build base snapshot'), () => { }, ); }); + +// --------------------------------- UTILITIES --------------------------------- + +type TestCase = [ + string, // name + string, // partial query + DataCubeColumn[], // source columns + ( + | (( + query: V1_ValueSpecification, + columns: DataCubeColumn[], + ) => Promise) + | undefined + ), // configuration builder + string | undefined, // error + ((snapshot: DataCubeQuerySnapshot) => void) | undefined, // extra checks on snapshot +]; + +function _case( + name: string, + data: { + query: string; + columns?: string[] | undefined; + configurationBuilder?: + | (( + query: V1_ValueSpecification, + columns: DataCubeColumn[], + ) => Promise) + | undefined; + error?: string | undefined; + validator?: ((snapshot: DataCubeQuerySnapshot) => void) | undefined; + }, +): TestCase { + return [ + name, + data.query, + data.columns?.map((entry) => { + const parts = entry.split(':'); + return { + name: (parts[0] as string).trim(), + type: (parts[1] as string).trim(), + }; + }) ?? [], + data.configurationBuilder, + data.error, + data.validator, + ]; +} + +function _checkFilterOperator(operator: DataCubeQueryFilterOperator) { + return (snapshot: DataCubeQuerySnapshot) => { + expect( + ( + snapshot.data.filter + ?.conditions[0] as DataCubeQuerySnapshotFilterCondition + ).operator, + ).toBe(operator); + }; +} + +/** + * Generates a default configuration based on the specified partial query and source columns. + * And therefore, it saves us from having to specify a JSON for the configuration when we want + * to test configuration validation. + * + * This is often used to generate a default configuration based on the query provided in the test, + * then we can provide extra steps to modify and tailor the configuration to the specific test. + */ +async function _generateDefaultConfiguration( + query: V1_ValueSpecification, + columns: DataCubeColumn[], +) { + const engine = new TEST__DataCubeEngine(); + const baseQuery = new DataCubeQuery(); + const source = new INTERNAL__DataCubeSource(); + source.columns = columns; + baseQuery.configuration = undefined; + + try { + const snapshot = await validateAndBuildQuerySnapshot( + query, + source, + baseQuery, + engine, + ); + + return DataCubeConfiguration.serialization.fromJson( + snapshot.data.configuration, + ); + } catch (error) { + assertErrorThrown(error); + throw new Error( + `Can't generate default configuration. Error: ${error.message}`, + ); + } +} diff --git a/packages/legend-data-cube/src/stores/core/__tests__/DataCubeTestUtils.ts b/packages/legend-data-cube/src/stores/core/__tests__/DataCubeTestUtils.ts index d1bdc1b5db..c25073da71 100644 --- a/packages/legend-data-cube/src/stores/core/__tests__/DataCubeTestUtils.ts +++ b/packages/legend-data-cube/src/stores/core/__tests__/DataCubeTestUtils.ts @@ -14,12 +14,21 @@ * limitations under the License. */ -import type { - V1_ValueSpecification, - V1_Lambda, - V1_AppliedFunction, +import { + type V1_ValueSpecification, + type V1_Lambda, + type V1_AppliedFunction, + V1_relationTypeModelSchema, + V1_getGenericTypeFullPath, + V1_buildEngineError, + V1_EngineError, + V1_buildParserError, } from '@finos/legend-graph'; -import type { PlainObject } from '@finos/legend-shared'; +import { + assertErrorThrown, + HttpStatus, + type PlainObject, +} from '@finos/legend-shared'; import { DataCubeEngine, type CompletionItem, @@ -29,13 +38,16 @@ import { } from '../DataCubeEngine.js'; import type { DataCubeSource } from '../model/DataCubeSource.js'; import { + ENGINE_TEST_SUPPORT__getLambdaRelationType, ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification, ENGINE_TEST_SUPPORT__JSONToGrammar_valueSpecification, + ENGINE_TEST_SUPPORT__NetworkClientError, } from '@finos/legend-graph/test'; import { _deserializeValueSpecification, _serializeValueSpecification, } from '../DataCubeQueryBuilderUtils.js'; +import { deserialize } from 'serializr'; export class TEST__DataCubeEngine extends DataCubeEngine { override async processQuerySource( @@ -48,12 +60,28 @@ export class TEST__DataCubeEngine extends DataCubeEngine { code: string, returnSourceInformation?: boolean | undefined, ): Promise { - return _deserializeValueSpecification( - await ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification( - code, - returnSourceInformation, - ), - ); + try { + return _deserializeValueSpecification( + await ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification( + code, + returnSourceInformation, + ), + ); + } catch (error) { + assertErrorThrown(error); + if ( + error instanceof ENGINE_TEST_SUPPORT__NetworkClientError && + error.status === HttpStatus.BAD_REQUEST + ) { + const engineError = V1_buildParserError( + V1_EngineError.serialization.fromJson( + error.response?.data as PlainObject, + ), + ); + throw engineError; + } + throw error; + } } override async getValueSpecificationCode( @@ -74,6 +102,41 @@ export class TEST__DataCubeEngine extends DataCubeEngine { throw new Error('Method not implemented.'); } + override async getQueryRelationReturnType( + query: V1_Lambda, + source: DataCubeSource, + ): Promise { + try { + const relationType = deserialize( + V1_relationTypeModelSchema, + await ENGINE_TEST_SUPPORT__getLambdaRelationType( + _serializeValueSpecification(query), + {}, + ), + ); + return { + columns: relationType.columns.map((column) => ({ + name: column.name, + type: V1_getGenericTypeFullPath(column.genericType), + })), + }; + } catch (error) { + assertErrorThrown(error); + if ( + error instanceof ENGINE_TEST_SUPPORT__NetworkClientError && + error.status === HttpStatus.BAD_REQUEST + ) { + const engineError = V1_buildEngineError( + V1_EngineError.serialization.fromJson( + error.response?.data as PlainObject, + ), + ); + throw engineError; + } + throw error; + } + } + override async getQueryCodeRelationReturnType( code: string, baseQuery: V1_ValueSpecification, diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation.ts b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation.ts index 9b4586631a..bf98b3c12c 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation.ts +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation.ts @@ -14,24 +14,11 @@ * limitations under the License. */ -import { guaranteeNonNullable } from '@finos/legend-shared'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { type V1_ColSpec } from '@finos/legend-graph'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; - -// --------------------------------- UTILITIES --------------------------------- - -export function getAggregateOperation( - operator: string, - aggregateOperations: DataCubeQueryAggregateOperation[], -) { - return guaranteeNonNullable( - aggregateOperations.find((op) => op.operator === operator), - `Can't find aggregate operation '${operator}'`, - ); -} - -// --------------------------------- CONTRACT --------------------------------- +import type { DataCubeOperationValue } from '../DataCubeQueryEngine.js'; +import type { DataCubeQuerySnapshotAggregateColumn } from '../DataCubeQuerySnapshot.js'; export abstract class DataCubeQueryAggregateOperation { abstract get label(): React.ReactNode; @@ -40,8 +27,44 @@ export abstract class DataCubeQueryAggregateOperation { abstract get operator(): string; abstract isCompatibleWithColumn(column: DataCubeColumn): boolean; + abstract isCompatibleWithParameterValues( + values: DataCubeOperationValue[], + ): boolean; + abstract generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[]; + + abstract buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ): DataCubeQuerySnapshotAggregateColumn | undefined; + + protected _finalizeAggregateColumnSnapshot( + data: + | { + column: DataCubeColumn; + paramterValues: DataCubeOperationValue[]; + } + | undefined, + ): DataCubeQuerySnapshotAggregateColumn | undefined { + if (!data) { + return undefined; + } + const { column, paramterValues } = data; + if ( + !this.isCompatibleWithColumn(column) || + !this.isCompatibleWithParameterValues(paramterValues) + ) { + return undefined; + } + return { + ...column, + operator: this.operator, + parameterValues: paramterValues, + }; + } - abstract buildAggregateColumn( + abstract buildAggregateColumnExpression( column: DataCubeColumnConfiguration, ): V1_ColSpec | undefined; } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Average.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Average.tsx index b3dad4c114..bb76e7442b 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Average.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Average.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Average extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__Average extends DataCubeQueryAggre return DataCubeQueryAggregateOperator.AVERAGE; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.AVERAGE); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.AVERAGE, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.AVERAGE); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Count.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Count.tsx index 5a16b48ca5..d0a6e98193 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Count.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Count.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Count extends DataCubeQueryAggregateOperation { override get label() { @@ -42,7 +45,7 @@ export class DataCubeQueryAggregateOperation__Count extends DataCubeQueryAggrega return DataCubeQueryAggregateOperator.COUNT; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [ // NOTE: technically all data types are suported, // but we can't because we must preserve the type @@ -55,7 +58,26 @@ export class DataCubeQueryAggregateOperation__Count extends DataCubeQueryAggrega ]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.COUNT); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.COUNT, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.COUNT); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__First.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__First.tsx index 2ba989d15b..73d53b0a26 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__First.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__First.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__First extends DataCubeQueryAggregateOperation { override get label() { @@ -42,7 +45,7 @@ export class DataCubeQueryAggregateOperation__First extends DataCubeQueryAggrega return DataCubeQueryAggregateOperator.FIRST; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [ DataCubeColumnDataType.TEXT, DataCubeColumnDataType.NUMBER, @@ -51,7 +54,26 @@ export class DataCubeQueryAggregateOperation__First extends DataCubeQueryAggrega ]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.FIRST); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.FIRST, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.FIRST); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__JoinStrings.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__JoinStrings.tsx index 48b46fea24..f349e6ba7d 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__JoinStrings.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__JoinStrings.tsx @@ -15,24 +15,19 @@ */ import type { DataCubeColumn } from '../model/DataCubeColumn.js'; -import { PRIMITIVE_TYPE } from '@finos/legend-graph'; +import { PRIMITIVE_TYPE, type V1_ColSpec } from '@finos/legend-graph'; import { DataCubeQueryAggregateOperation } from './DataCubeQueryAggregateOperation.js'; import { DataCubeQueryAggregateOperator, DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { - _colSpec, - _function, - _functionName, - _lambda, - _primitiveValue, - _property, - _var, -} from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryAggregateOperation__JoinStrings extends DataCubeQueryAggregateOperation { override get label() { @@ -51,7 +46,7 @@ export class DataCubeQueryAggregateOperation__JoinStrings extends DataCubeQueryA return DataCubeQueryAggregateOperator.JOIN_STRINGS; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [ // NOTE: technically all data types should be suported, // i.e. we can use meta::pure::functions::string::makeString @@ -61,21 +56,41 @@ export class DataCubeQueryAggregateOperation__JoinStrings extends DataCubeQueryA ]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - const variable = _var(); - return _colSpec( - column.name, - _lambda([variable], [_property(column.name, variable)]), - _lambda( - [variable], - [ - _function(_functionName(DataCubeFunction.JOIN_STRINGS), [ - variable, - // TODO: we might want to support customizing the delimiter in this case - _primitiveValue(PRIMITIVE_TYPE.STRING, ','), - ]), - ], - ), + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return ( + values.length === 1 && + values[0] !== undefined && + ofDataType(values[0].type, [DataCubeColumnDataType.TEXT]) && + !Array.isArray(values[0].value) && + isString(values[0].value) + ); + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return [ + { + type: PRIMITIVE_TYPE.STRING, + value: '', + }, + ]; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.JOIN_STRINGS, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base( + column, + DataCubeFunction.JOIN_STRINGS, + column.aggregationParameters, ); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Last.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Last.tsx index c2581594cf..ebc80858e5 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Last.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Last.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Last extends DataCubeQueryAggregateOperation { override get label() { @@ -42,7 +45,7 @@ export class DataCubeQueryAggregateOperation__Last extends DataCubeQueryAggregat return DataCubeQueryAggregateOperator.LAST; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [ DataCubeColumnDataType.TEXT, DataCubeColumnDataType.NUMBER, @@ -51,7 +54,26 @@ export class DataCubeQueryAggregateOperation__Last extends DataCubeQueryAggregat ]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.LAST); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.LAST, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.LAST); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Max.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Max.tsx index 7d8a96b6bf..941cf83b71 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Max.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Max.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Max extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__Max extends DataCubeQueryAggregate return DataCubeQueryAggregateOperator.MAX; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.MAX); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.MAX, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.MAX); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Min.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Min.tsx index 4a567d801e..450773ac77 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Min.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Min.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Min extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__Min extends DataCubeQueryAggregate return DataCubeQueryAggregateOperator.MIN; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.MIN); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.MIN, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.MIN); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevPopulation.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevPopulation.tsx index 016b25ecba..83dcbb6938 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevPopulation.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevPopulation.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__StdDevPopulation extends DataCubeQueryAggregateOperation { override get label() { @@ -42,14 +45,34 @@ export class DataCubeQueryAggregateOperation__StdDevPopulation extends DataCubeQ return DataCubeQueryAggregateOperator.STANDARD_DEVIATION_POPULATION; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic( - column, - DataCubeFunction.STANDARD_DEVIATION_POPULATION, + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base( + colSpec, + DataCubeFunction.STANDARD_DEVIATION_POPULATION, + columnGetter, + ), ); } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.STANDARD_DEVIATION_POPULATION); + } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevSample.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevSample.tsx index 18d8d18ff8..62ce35c05d 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevSample.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__StdDevSample.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__StdDevSample extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,34 @@ export class DataCubeQueryAggregateOperation__StdDevSample extends DataCubeQuery return DataCubeQueryAggregateOperator.STANDARD_DEVIATION_SAMPLE; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.STANDARD_DEVIATION_SAMPLE); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base( + colSpec, + DataCubeFunction.STANDARD_DEVIATION_SAMPLE, + columnGetter, + ), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.STANDARD_DEVIATION_SAMPLE); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Sum.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Sum.tsx index 6dc197289b..f39d3659d3 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Sum.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__Sum.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import { type V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__Sum extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__Sum extends DataCubeQueryAggregate return DataCubeQueryAggregateOperator.SUM; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.SUM); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.SUM, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.SUM); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__UniqueValue.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__UniqueValue.tsx index 82aa791142..bdb9ddb683 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__UniqueValue.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__UniqueValue.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__UniqueValue extends DataCubeQueryAggregateOperation { override get label() { @@ -42,7 +45,7 @@ export class DataCubeQueryAggregateOperation__UniqueValue extends DataCubeQueryA return DataCubeQueryAggregateOperator.UNIQUE; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [ DataCubeColumnDataType.TEXT, DataCubeColumnDataType.NUMBER, @@ -51,7 +54,26 @@ export class DataCubeQueryAggregateOperation__UniqueValue extends DataCubeQueryA ]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.UNIQUE_VALUE_ONLY); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.UNIQUE_VALUE_ONLY, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.UNIQUE_VALUE_ONLY); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VariancePopulation.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VariancePopulation.tsx index 09eba49908..7ac2851e3c 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VariancePopulation.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VariancePopulation.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__VariancePopulation extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__VariancePopulation extends DataCub return DataCubeQueryAggregateOperator.VARIANCE_POPULATION; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.VARIANCE_POPULATION); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.VARIANCE_POPULATION, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.VARIANCE_POPULATION); } } diff --git a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VarianceSample.tsx b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VarianceSample.tsx index 552e492833..8bc598cb3d 100644 --- a/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VarianceSample.tsx +++ b/packages/legend-data-cube/src/stores/core/aggregation/DataCubeQueryAggregateOperation__VarianceSample.tsx @@ -21,9 +21,12 @@ import { DataCubeColumnDataType, DataCubeFunction, ofDataType, + type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; -import { _aggCol_basic } from '../DataCubeQueryBuilderUtils.js'; +import { _aggCol_base } from '../DataCubeQueryBuilderUtils.js'; import type { DataCubeColumnConfiguration } from '../model/DataCubeConfiguration.js'; +import type { V1_ColSpec } from '@finos/legend-graph'; +import { _agg_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryAggregateOperation__VarianceSample extends DataCubeQueryAggregateOperation { override get label() { @@ -42,11 +45,30 @@ export class DataCubeQueryAggregateOperation__VarianceSample extends DataCubeQue return DataCubeQueryAggregateOperator.VARIANCE_SAMPLE; } - isCompatibleWithColumn(column: DataCubeColumn) { + override isCompatibleWithColumn(column: DataCubeColumn) { return ofDataType(column.type, [DataCubeColumnDataType.NUMBER]); } - buildAggregateColumn(column: DataCubeColumnConfiguration) { - return _aggCol_basic(column, DataCubeFunction.VARIANCE_SAMPLE); + override isCompatibleWithParameterValues(values: DataCubeOperationValue[]) { + return !values.length; + } + + override generateDefaultParameterValues( + column: DataCubeColumn, + ): DataCubeOperationValue[] { + return []; + } + + override buildAggregateColumnSnapshot( + colSpec: V1_ColSpec, + columnGetter: (name: string) => DataCubeColumn, + ) { + return this._finalizeAggregateColumnSnapshot( + _agg_base(colSpec, DataCubeFunction.VARIANCE_SAMPLE, columnGetter), + ); + } + + override buildAggregateColumnExpression(column: DataCubeColumnConfiguration) { + return _aggCol_base(column, DataCubeFunction.VARIANCE_SAMPLE); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation.ts b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation.ts index a77b515181..94a25b35c3 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation.ts +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation.ts @@ -14,60 +14,10 @@ * limitations under the License. */ -import { - formatDate, - guaranteeNonNullable, - UnsupportedOperationError, -} from '@finos/legend-shared'; import type { DataCubeOperationValue } from '../DataCubeQueryEngine.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; -import { - DATE_FORMAT, - DATE_TIME_FORMAT, - PRIMITIVE_TYPE, - type V1_AppliedFunction, -} from '@finos/legend-graph'; - -// --------------------------------- UTILITIES --------------------------------- - -export function _defaultPrimitiveTypeValue(type: string): unknown { - switch (type) { - case PRIMITIVE_TYPE.STRING: - return ''; - case PRIMITIVE_TYPE.BOOLEAN: - return false; - case PRIMITIVE_TYPE.BYTE: - return btoa(''); - case PRIMITIVE_TYPE.NUMBER: - case PRIMITIVE_TYPE.DECIMAL: - case PRIMITIVE_TYPE.FLOAT: - case PRIMITIVE_TYPE.INTEGER: - case PRIMITIVE_TYPE.BINARY: - return 0; - case PRIMITIVE_TYPE.DATE: - case PRIMITIVE_TYPE.STRICTDATE: - return formatDate(new Date(Date.now()), DATE_FORMAT); - case PRIMITIVE_TYPE.DATETIME: - return formatDate(new Date(Date.now()), DATE_TIME_FORMAT); - default: - throw new UnsupportedOperationError( - `Can't generate value for type '${type}'`, - ); - } -} - -export function getFilterOperation( - operator: string, - operators: DataCubeQueryFilterOperation[], -) { - return guaranteeNonNullable( - operators.find((op) => op.operator === operator), - `Can't find filter operation '${operator}'`, - ); -} - -// --------------------------------- CONTRACT --------------------------------- +import { type V1_AppliedFunction } from '@finos/legend-graph'; export abstract class DataCubeQueryFilterOperation { abstract get label(): React.ReactNode; @@ -77,7 +27,6 @@ export abstract class DataCubeQueryFilterOperation { abstract isCompatibleWithColumn(column: DataCubeColumn): boolean; abstract isCompatibleWithValue(value: DataCubeOperationValue): boolean; - abstract generateDefaultValue(column: DataCubeColumn): DataCubeOperationValue; abstract buildConditionSnapshot( diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Contain.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Contain.tsx index 379dcd13d5..ab5086385f 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Contain.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Contain.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,9 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__Contain extends DataCubeQueryFilterOperation { override get label() { @@ -63,7 +61,8 @@ export class DataCubeQueryFilterOperation__Contain extends DataCubeQueryFilterOp value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -79,14 +78,18 @@ export class DataCubeQueryFilterOperation__Contain extends DataCubeQueryFilterOp columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition(expression, columnGetter, DataCubeFunction.CONTAINS), + _filterCondition_base( + expression, + DataCubeFunction.CONTAINS, + columnGetter, + ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.CONTAINS), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__ContainCaseInsensitive.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__ContainCaseInsensitive.tsx index 0f84dd9649..0e54b794e1 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__ContainCaseInsensitive.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__ContainCaseInsensitive.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -32,11 +30,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _caseSensitiveBaseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_caseSensitive } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__ContainCaseInsensitive extends DataCubeQueryFilterOperation { override get label() { @@ -64,7 +61,8 @@ export class DataCubeQueryFilterOperation__ContainCaseInsensitive extends DataCu value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -80,22 +78,21 @@ export class DataCubeQueryFilterOperation__ContainCaseInsensitive extends DataCu columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( + _filterCondition_caseSensitive( expression, - columnGetter, DataCubeFunction.CONTAINS, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.CONTAINS), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWith.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWith.tsx index da2b276701..e045034e0b 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWith.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWith.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,9 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__EndWith extends DataCubeQueryFilterOperation { override get label() { @@ -63,7 +61,8 @@ export class DataCubeQueryFilterOperation__EndWith extends DataCubeQueryFilterOp value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -79,10 +78,10 @@ export class DataCubeQueryFilterOperation__EndWith extends DataCubeQueryFilterOp columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.ENDS_WITH, + columnGetter, ), ); } @@ -90,7 +89,7 @@ export class DataCubeQueryFilterOperation__EndWith extends DataCubeQueryFilterOp buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.ENDS_WITH), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWithCaseInsensitive.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWithCaseInsensitive.tsx index ac0570f846..1ca94eb2de 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWithCaseInsensitive.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EndWithCaseInsensitive.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -32,11 +30,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _caseSensitiveBaseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_caseSensitive } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__EndWithCaseInsensitive extends DataCubeQueryFilterOperation { override get label() { @@ -64,7 +61,8 @@ export class DataCubeQueryFilterOperation__EndWithCaseInsensitive extends DataCu value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -80,22 +78,21 @@ export class DataCubeQueryFilterOperation__EndWithCaseInsensitive extends DataCu columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( + _filterCondition_caseSensitive( expression, - columnGetter, DataCubeFunction.ENDS_WITH, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.ENDS_WITH), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Equal.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Equal.tsx index 9ecc70611b..599b3c85d5 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Equal.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__Equal.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,8 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__Equal extends DataCubeQueryFilterOperation { override get label() { @@ -89,14 +86,14 @@ export class DataCubeQueryFilterOperation__Equal extends DataCubeQueryFilterOper columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition(expression, columnGetter, DataCubeFunction.EQUAL), + _filterCondition_base(expression, DataCubeFunction.EQUAL, columnGetter), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.EQUAL), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitive.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitive.tsx index 5676cae4d5..609a7bd7dc 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitive.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitive.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -32,11 +30,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _caseSensitiveBaseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_caseSensitive } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__EqualCaseInsensitive extends DataCubeQueryFilterOperation { override get label() { @@ -64,7 +61,8 @@ export class DataCubeQueryFilterOperation__EqualCaseInsensitive extends DataCube value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -80,22 +78,21 @@ export class DataCubeQueryFilterOperation__EqualCaseInsensitive extends DataCube columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( + _filterCondition_caseSensitive( expression, - columnGetter, DataCubeFunction.EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.EQUAL), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn.tsx index ecdeb13073..dd2ac0d78a 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _caseSensitiveBaseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_caseSensitive } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn extends DataCubeQueryFilterOperation { override get label() { @@ -76,22 +75,21 @@ export class DataCubeQueryFilterOperation__EqualCaseInsensitiveColumn extends Da columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( + _filterCondition_caseSensitive( expression, - columnGetter, DataCubeFunction.EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.EQUAL), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualColumn.tsx index 08d60d9b64..fe008b663e 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__EqualColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__EqualColumn extends DataCubeQueryFilterOperation { override get label() { @@ -81,15 +80,14 @@ export class DataCubeQueryFilterOperation__EqualColumn extends DataCubeQueryFilt columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition(expression, columnGetter, DataCubeFunction.EQUAL), + _filterCondition_base(expression, DataCubeFunction.EQUAL, columnGetter), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.EQUAL), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.tsx index 35907b0135..e65dae6bfc 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,8 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__GreaterThan extends DataCubeQueryFilterOperation { override get label() { @@ -87,10 +84,10 @@ export class DataCubeQueryFilterOperation__GreaterThan extends DataCubeQueryFilt columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.GREATER_THAN, + columnGetter, ), ); } @@ -98,7 +95,7 @@ export class DataCubeQueryFilterOperation__GreaterThan extends DataCubeQueryFilt buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.GREATER_THAN), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanColumn.tsx index 4d0d9d2acf..995202f0e7 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__GreaterThanColumn extends DataCubeQueryFilterOperation { override get label() { @@ -80,19 +79,18 @@ export class DataCubeQueryFilterOperation__GreaterThanColumn extends DataCubeQue columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.GREATER_THAN, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.GREATER_THAN), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.tsx index 01c3a78302..459ca415c1 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,8 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__GreaterThanOrEqual extends DataCubeQueryFilterOperation { override get label() { @@ -87,10 +84,10 @@ export class DataCubeQueryFilterOperation__GreaterThanOrEqual extends DataCubeQu columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.GREATER_THAN_OR_EQUAL, + columnGetter, ), ); } @@ -98,7 +95,7 @@ export class DataCubeQueryFilterOperation__GreaterThanOrEqual extends DataCubeQu buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.GREATER_THAN_OR_EQUAL), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqualColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqualColumn.tsx index 4b68a2380c..f5fbeffcfe 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqualColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqualColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__GreaterThanOrEqualColumn extends DataCubeQueryFilterOperation { override get label() { @@ -80,19 +79,18 @@ export class DataCubeQueryFilterOperation__GreaterThanOrEqualColumn extends Data columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.GREATER_THAN_OR_EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.GREATER_THAN_OR_EQUAL), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNotNull.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNotNull.tsx index 9b94775c7f..f9757e2059 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNotNull.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNotNull.tsx @@ -33,7 +33,7 @@ import { import { type V1_AppliedFunction } from '@finos/legend-graph'; import { _unwrapNotFilterCondition, - _baseFilterCondition, + _filterCondition_base, } from '../DataCubeQuerySnapshotBuilderUtils.js'; import { returnUndefOnError } from '@finos/legend-shared'; @@ -80,14 +80,12 @@ export class DataCubeQueryFilterOperation__IsNotNull extends DataCubeQueryFilter expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition(unwrapped, columnGetter, DataCubeFunction.IS_EMPTY), + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), + DataCubeFunction.IS_EMPTY, + columnGetter, + ), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNull.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNull.tsx index a129b640a2..bbf6efed26 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNull.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__IsNull.tsx @@ -30,7 +30,7 @@ import { _property, } from '../DataCubeQueryBuilderUtils.js'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__IsNull extends DataCubeQueryFilterOperation { override get label() { @@ -76,7 +76,11 @@ export class DataCubeQueryFilterOperation__IsNull extends DataCubeQueryFilterOpe columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition(expression, columnGetter, DataCubeFunction.IS_EMPTY), + _filterCondition_base( + expression, + DataCubeFunction.IS_EMPTY, + columnGetter, + ), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThan.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThan.tsx index 3dcd361bd8..7d856c1de5 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThan.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThan.tsx @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -26,6 +23,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -34,9 +32,8 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__LessThan extends DataCubeQueryFilterOperation { override get label() { @@ -88,10 +85,10 @@ export class DataCubeQueryFilterOperation__LessThan extends DataCubeQueryFilterO columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.LESS_THAN, + columnGetter, ), ); } @@ -99,7 +96,7 @@ export class DataCubeQueryFilterOperation__LessThan extends DataCubeQueryFilterO buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.LESS_THAN), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanColumn.tsx index 22e5d2152a..4ef95c6487 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__LessThanColumn extends DataCubeQueryFilterOperation { override get label() { @@ -80,19 +79,18 @@ export class DataCubeQueryFilterOperation__LessThanColumn extends DataCubeQueryF columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.LESS_THAN, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.LESS_THAN), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.tsx index 621374cfaf..a6a1a4d7f1 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,8 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__LessThanOrEqual extends DataCubeQueryFilterOperation { override get label() { @@ -87,10 +84,10 @@ export class DataCubeQueryFilterOperation__LessThanOrEqual extends DataCubeQuery columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.LESS_THAN_OR_EQUAL, + columnGetter, ), ); } @@ -98,7 +95,7 @@ export class DataCubeQueryFilterOperation__LessThanOrEqual extends DataCubeQuery buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.LESS_THAN_OR_EQUAL), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqualColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqualColumn.tsx index 8c1927e00b..6d4d0204fb 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqualColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqualColumn.tsx @@ -29,11 +29,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, isString } from '@finos/legend-shared'; +import { isString } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__LessThanOrEqualColumn extends DataCubeQueryFilterOperation { override get label() { @@ -80,19 +79,18 @@ export class DataCubeQueryFilterOperation__LessThanOrEqualColumn extends DataCub columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.LESS_THAN_OR_EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.LESS_THAN_OR_EQUAL), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotContain.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotContain.tsx index cc60c0acbb..cafdb340aa 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotContain.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotContain.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -34,11 +32,11 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, returnUndefOnError } from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { _unwrapNotFilterCondition, - _baseFilterCondition, + _filterCondition_base, } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__NotContain extends DataCubeQueryFilterOperation { @@ -67,7 +65,8 @@ export class DataCubeQueryFilterOperation__NotContain extends DataCubeQueryFilte value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -82,14 +81,12 @@ export class DataCubeQueryFilterOperation__NotContain extends DataCubeQueryFilte expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition(unwrapped, columnGetter, DataCubeFunction.CONTAINS), + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), + DataCubeFunction.CONTAINS, + columnGetter, + ), ); } @@ -97,7 +94,7 @@ export class DataCubeQueryFilterOperation__NotContain extends DataCubeQueryFilte return _not( _function(_functionName(DataCubeFunction.CONTAINS), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEndWith.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEndWith.tsx index 86e72eba31..61e7ea04ee 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEndWith.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEndWith.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -34,11 +32,11 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, returnUndefOnError } from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { _unwrapNotFilterCondition, - _baseFilterCondition, + _filterCondition_base, } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__NotEndWith extends DataCubeQueryFilterOperation { @@ -67,7 +65,8 @@ export class DataCubeQueryFilterOperation__NotEndWith extends DataCubeQueryFilte value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -82,14 +81,12 @@ export class DataCubeQueryFilterOperation__NotEndWith extends DataCubeQueryFilte expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition(unwrapped, columnGetter, DataCubeFunction.ENDS_WITH), + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), + DataCubeFunction.ENDS_WITH, + columnGetter, + ), ); } @@ -97,7 +94,7 @@ export class DataCubeQueryFilterOperation__NotEndWith extends DataCubeQueryFilte return _not( _function(_functionName(DataCubeFunction.ENDS_WITH), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.tsx index 7d4c941514..011cf0caf4 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -34,11 +32,11 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, returnUndefOnError } from '@finos/legend-shared'; +import { returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { _unwrapNotFilterCondition, - _baseFilterCondition, + _filterCondition_base, } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__NotEqual extends DataCubeQueryFilterOperation { @@ -92,14 +90,12 @@ export class DataCubeQueryFilterOperation__NotEqual extends DataCubeQueryFilterO expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition(unwrapped, columnGetter, DataCubeFunction.EQUAL), + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), + DataCubeFunction.EQUAL, + columnGetter, + ), ); } @@ -107,7 +103,7 @@ export class DataCubeQueryFilterOperation__NotEqual extends DataCubeQueryFilterO return _not( _function(_functionName(DataCubeFunction.EQUAL), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitive.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitive.tsx index e22f5a6e1f..e755105186 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitive.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitive.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,12 +31,11 @@ import { _not, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, returnUndefOnError } from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { - _caseSensitiveBaseFilterCondition, + _filterCondition_caseSensitive, _unwrapNotFilterCondition, } from '../DataCubeQuerySnapshotBuilderUtils.js'; @@ -68,7 +65,8 @@ export class DataCubeQueryFilterOperation__NotEqualCaseInsensitive extends DataC value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -83,30 +81,23 @@ export class DataCubeQueryFilterOperation__NotEqualCaseInsensitive extends DataC expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( - unwrapped, - columnGetter, + _filterCondition_caseSensitive( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), DataCubeFunction.EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _not( _function(_functionName(DataCubeFunction.EQUAL), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]), ); diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitiveColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitiveColumn.tsx index 745247541e..39bd3dc8b5 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitiveColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualCaseInsensitiveColumn.tsx @@ -30,16 +30,11 @@ import { _not, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { - guaranteeNonNullable, - isString, - returnUndefOnError, -} from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { - _caseSensitiveBaseFilterCondition, + _filterCondition_caseSensitive, _unwrapNotFilterCondition, } from '../DataCubeQuerySnapshotBuilderUtils.js'; @@ -83,30 +78,23 @@ export class DataCubeQueryFilterOperation__NotEqualCaseInsensitiveColumn extends expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( - unwrapped, - columnGetter, + _filterCondition_caseSensitive( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), DataCubeFunction.EQUAL, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _not( _function(_functionName(DataCubeFunction.EQUAL), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]), ); diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualColumn.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualColumn.tsx index 5dad269a20..36634e37c0 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualColumn.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqualColumn.tsx @@ -30,16 +30,11 @@ import { _not, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { - guaranteeNonNullable, - isString, - returnUndefOnError, -} from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { - _baseFilterCondition, + _filterCondition_base, _unwrapNotFilterCondition, } from '../DataCubeQuerySnapshotBuilderUtils.js'; @@ -88,23 +83,20 @@ export class DataCubeQueryFilterOperation__NotEqualColumn extends DataCubeQueryF expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition(unwrapped, columnGetter, DataCubeFunction.EQUAL), + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), + DataCubeFunction.EQUAL, + columnGetter, + ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _not( _function(_functionName(DataCubeFunction.EQUAL), [ - _property(condition.name, variable), - _value(guaranteeNonNullable(condition.value), variable), + _property(condition.name), + _value(condition.value), ]), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotStartWith.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotStartWith.tsx index 5027fab27c..c8feb03bb7 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotStartWith.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__NotStartWith.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -34,11 +32,11 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable, returnUndefOnError } from '@finos/legend-shared'; +import { isString, returnUndefOnError } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; import { _unwrapNotFilterCondition, - _baseFilterCondition, + _filterCondition_base, } from '../DataCubeQuerySnapshotBuilderUtils.js'; export class DataCubeQueryFilterOperation__NotStartWith extends DataCubeQueryFilterOperation { @@ -67,7 +65,8 @@ export class DataCubeQueryFilterOperation__NotStartWith extends DataCubeQueryFil value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -82,17 +81,11 @@ export class DataCubeQueryFilterOperation__NotStartWith extends DataCubeQueryFil expression: V1_AppliedFunction, columnGetter: (name: string) => DataCubeColumn, ) { - const unwrapped = returnUndefOnError(() => - _unwrapNotFilterCondition(expression), - ); - if (!unwrapped) { - return undefined; - } return this._finalizeConditionSnapshot( - _baseFilterCondition( - unwrapped, - columnGetter, + _filterCondition_base( + returnUndefOnError(() => _unwrapNotFilterCondition(expression)), DataCubeFunction.STARTS_WITH, + columnGetter, ), ); } @@ -101,7 +94,7 @@ export class DataCubeQueryFilterOperation__NotStartWith extends DataCubeQueryFil return _not( _function(_functionName(DataCubeFunction.STARTS_WITH), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]), ); } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWith.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWith.tsx index f102ff2905..0dd87586d4 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWith.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWith.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -33,9 +31,9 @@ import { _property, _value, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _baseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_base } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__StartWith extends DataCubeQueryFilterOperation { override get label() { @@ -63,7 +61,8 @@ export class DataCubeQueryFilterOperation__StartWith extends DataCubeQueryFilter value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -79,10 +78,10 @@ export class DataCubeQueryFilterOperation__StartWith extends DataCubeQueryFilter columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _baseFilterCondition( + _filterCondition_base( expression, - columnGetter, DataCubeFunction.STARTS_WITH, + columnGetter, ), ); } @@ -90,7 +89,7 @@ export class DataCubeQueryFilterOperation__StartWith extends DataCubeQueryFilter buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { return _function(_functionName(DataCubeFunction.STARTS_WITH), [ _property(condition.name), - _value(guaranteeNonNullable(condition.value)), + _value(condition.value), ]); } } diff --git a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWithCaseInsensitive.tsx b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWithCaseInsensitive.tsx index 4185d92d8c..a3f52c013b 100644 --- a/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWithCaseInsensitive.tsx +++ b/packages/legend-data-cube/src/stores/core/filter/DataCubeQueryFilterOperation__StartWithCaseInsensitive.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DataCubeQueryFilterOperation, - _defaultPrimitiveTypeValue, -} from './DataCubeQueryFilterOperation.js'; +import { DataCubeQueryFilterOperation } from './DataCubeQueryFilterOperation.js'; import type { DataCubeQuerySnapshotFilterCondition } from '../DataCubeQuerySnapshot.js'; import type { DataCubeColumn } from '../model/DataCubeColumn.js'; import { @@ -25,6 +22,7 @@ import { DataCubeQueryFilterOperator, isPrimitiveType, ofDataType, + _defaultPrimitiveTypeValue, type DataCubeOperationValue, } from '../DataCubeQueryEngine.js'; import { @@ -32,11 +30,10 @@ import { _functionName, _property, _value, - _var, } from '../DataCubeQueryBuilderUtils.js'; -import { guaranteeNonNullable } from '@finos/legend-shared'; import { type V1_AppliedFunction } from '@finos/legend-graph'; -import { _caseSensitiveBaseFilterCondition } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { _filterCondition_caseSensitive } from '../DataCubeQuerySnapshotBuilderUtils.js'; +import { isString } from '@finos/legend-shared'; export class DataCubeQueryFilterOperation__StartWithCaseInsensitive extends DataCubeQueryFilterOperation { override get label() { @@ -64,7 +61,8 @@ export class DataCubeQueryFilterOperation__StartWithCaseInsensitive extends Data value.value !== undefined && isPrimitiveType(value.type) && ofDataType(value.type, [DataCubeColumnDataType.TEXT]) && - !Array.isArray(value.value) + !Array.isArray(value.value) && + isString(value.value) ); } @@ -80,22 +78,21 @@ export class DataCubeQueryFilterOperation__StartWithCaseInsensitive extends Data columnGetter: (name: string) => DataCubeColumn, ) { return this._finalizeConditionSnapshot( - _caseSensitiveBaseFilterCondition( + _filterCondition_caseSensitive( expression, - columnGetter, DataCubeFunction.STARTS_WITH, + columnGetter, ), ); } buildConditionExpression(condition: DataCubeQuerySnapshotFilterCondition) { - const variable = _var(); return _function(_functionName(DataCubeFunction.STARTS_WITH), [ _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _property(condition.name, variable), + _property(condition.name), ]), _function(_functionName(DataCubeFunction.TO_LOWERCASE), [ - _value(guaranteeNonNullable(condition.value), variable), + _value(condition.value), ]), ]); } diff --git a/packages/legend-data-cube/src/stores/core/model/DataCubeColumn.ts b/packages/legend-data-cube/src/stores/core/model/DataCubeColumn.ts index 0e885252b4..048481a22c 100644 --- a/packages/legend-data-cube/src/stores/core/model/DataCubeColumn.ts +++ b/packages/legend-data-cube/src/stores/core/model/DataCubeColumn.ts @@ -21,7 +21,7 @@ export type DataCubeColumn = { export function _findCol( cols: T[] | undefined, - name: string, + name: string | undefined, ): T | undefined { return cols?.find((c) => c.name === name); } diff --git a/packages/legend-data-cube/src/stores/core/model/DataCubeConfiguration.ts b/packages/legend-data-cube/src/stores/core/model/DataCubeConfiguration.ts index e319c2d914..ff76e24de4 100644 --- a/packages/legend-data-cube/src/stores/core/model/DataCubeConfiguration.ts +++ b/packages/legend-data-cube/src/stores/core/model/DataCubeConfiguration.ts @@ -41,6 +41,7 @@ import { DEFAULT_PIVOT_STATISTIC_COLUMN_NAME, DEFAULT_TREE_COLUMN_SORT_DIRECTION, DEFAULT_REPORT_NAME, + type DataCubeQuerySortDirection, } from '../DataCubeQueryEngine.js'; import { SerializationFactory, @@ -48,6 +49,7 @@ import { uuid, } from '@finos/legend-shared'; import { createModelSchema, list, optional, primitive, raw } from 'serializr'; +import { _findCol } from './DataCubeColumn.js'; export type DataCubeConfigurationColorKey = | 'normal' @@ -114,7 +116,7 @@ export class DataCubeColumnConfiguration { aggregateOperator!: string; aggregationParameters: DataCubeOperationValue[] = []; excludedFromPivot = true; // this agrees with default column kind set as Dimension - pivotSortDirection?: string | undefined; + pivotSortDirection?: DataCubeQuerySortDirection | undefined; pivotStatisticColumnFunction?: string | undefined; constructor(name: string, type: string) { @@ -277,6 +279,10 @@ export class DataCubeConfiguration { }), ); + getColumn(name: string) { + return _findCol(this.columns, name); + } + serialize() { return DataCubeConfiguration.serialization.toJson(this); } diff --git a/packages/legend-data-cube/src/stores/services/DataCubeQuerySnapshotService.ts b/packages/legend-data-cube/src/stores/services/DataCubeQuerySnapshotService.ts index 74199b0419..ee1e16c4ab 100644 --- a/packages/legend-data-cube/src/stores/services/DataCubeQuerySnapshotService.ts +++ b/packages/legend-data-cube/src/stores/services/DataCubeQuerySnapshotService.ts @@ -18,6 +18,7 @@ import type { DataCubeQuerySnapshot } from '../core/DataCubeQuerySnapshot.js'; import { IllegalStateError, assertErrorThrown, + at, deepDiff, guaranteeNonNullable, } from '@finos/legend-shared'; @@ -112,7 +113,7 @@ export class DataCubeQuerySnapshotService { } get currentSnapshot() { - return guaranteeNonNullable(this._snapshots[this._snapshots.length - 1]); + return at(this._snapshots, this._snapshots.length - 1); } registerSubscriber(subscriber: DataCubeQuerySnapshotSubscriber) { diff --git a/packages/legend-data-cube/src/stores/services/DataCubeSettingService.tsx b/packages/legend-data-cube/src/stores/services/DataCubeSettingService.tsx index fad224b7b0..c2330afb22 100644 --- a/packages/legend-data-cube/src/stores/services/DataCubeSettingService.tsx +++ b/packages/legend-data-cube/src/stores/services/DataCubeSettingService.tsx @@ -144,6 +144,14 @@ export class DataCubeSettingService { type: DataCubeSettingType.BOOLEAN, defaultValue: false, } satisfies DataCubeSetting, + { + key: DataCubeSettingKey.DEBUGGER__USE_DEV_CLIENT_PROTOCOL_VERSION, + title: `Use development client protocol version: Enabled`, + description: `Specifies if development client protocol version (vX_X_X) should be used for execution`, + group: DataCubeSettingGroup.DEBUG, + type: DataCubeSettingType.BOOLEAN, + defaultValue: false, + } satisfies DataCubeSetting, { key: DataCubeSettingKey.DEBUGGER__ACTION__RELOAD, title: `Reload`, diff --git a/packages/legend-data-cube/src/stores/view/DataCubeViewState.ts b/packages/legend-data-cube/src/stores/view/DataCubeViewState.ts index b0d07b68f1..b2b140723d 100644 --- a/packages/legend-data-cube/src/stores/view/DataCubeViewState.ts +++ b/packages/legend-data-cube/src/stores/view/DataCubeViewState.ts @@ -134,12 +134,11 @@ export class DataCubeViewState { const partialQuery = await this.engine.parseValueSpecification( query.query, ); - const initialSnapshot = validateAndBuildQuerySnapshot( + const initialSnapshot = await validateAndBuildQuerySnapshot( partialQuery, source, query, - this.engine.filterOperations, - this.engine.aggregateOperations, + this.engine, ); this.snapshotService.broadcastSnapshot(initialSnapshot); this.dataCube.options?.onViewInitialized?.({ diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnPropertiesPanelState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnPropertiesPanelState.ts index b2cfdd150f..25cb4f64f3 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnPropertiesPanelState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnPropertiesPanelState.ts @@ -26,6 +26,7 @@ import { type PlainObject, } from '@finos/legend-shared'; import type { DataCubeConfiguration } from '../../core/model/DataCubeConfiguration.js'; +import { _findCol } from '../../core/model/DataCubeColumn.js'; export class DataCubeEditorColumnPropertiesPanelState implements DataCubeQueryEditorPanelState @@ -56,7 +57,7 @@ export class DataCubeEditorColumnPropertiesPanelState getColumnConfiguration(colName: string | undefined) { return guaranteeNonNullable( - this.columns.find((col) => col.name === colName), + _findCol(this.columns, colName), `Can't find configuration for column '${colName}'`, ); } @@ -70,7 +71,7 @@ export class DataCubeEditorColumnPropertiesPanelState } get selectedColumn() { - return this.columns.find((col) => col.name === this.selectedColumnName); + return _findCol(this.columns, this.selectedColumnName); } setShowAdvancedSettings(val: boolean) { diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnSelectorState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnSelectorState.ts index b673d8fd81..b6773fba1e 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnSelectorState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnSelectorState.ts @@ -17,7 +17,8 @@ import { makeObservable, observable, action, computed } from 'mobx'; import type { DataCubeEditorState } from './DataCubeEditorState.js'; import { guaranteeNonNullable } from '@finos/legend-shared'; -import { _sortByColName } from '../../core/model/DataCubeColumn.js'; +import { _findCol, _sortByColName } from '../../core/model/DataCubeColumn.js'; +import type { DataCubeQuerySortDirection } from '../../core/DataCubeQueryEngine.js'; export class DataCubeEditorColumnSelectorColumnState { readonly name: string; @@ -74,10 +75,7 @@ export abstract class DataCubeEditorColumnSelectorState< get availableColumnsForDisplay(): T[] { return this.availableColumns - .filter( - (column) => - !this.selectedColumns.find((col) => column.name === col.name), - ) + .filter((column) => !_findCol(this.selectedColumns, column.name)) .sort(_sortByColName); } @@ -103,10 +101,37 @@ export abstract class DataCubeEditorColumnSelectorState< getColumn(colName: string): T { return guaranteeNonNullable( - this.availableColumns.find((col) => col.name === colName), + _findCol(this.availableColumns, colName), `Can't find column '${colName}'`, ); } protected abstract cloneColumn(column: T): T; } + +export class DataCubeEditorSortColumnState extends DataCubeEditorColumnSelectorColumnState { + readonly onChange?: (() => void) | undefined; + direction: DataCubeQuerySortDirection; + + constructor( + name: string, + type: string, + direction: DataCubeQuerySortDirection, + onChange?: (() => void) | undefined, + ) { + super(name, type); + + makeObservable(this, { + direction: observable, + setDirection: action, + }); + + this.direction = direction; + this.onChange = onChange; + } + + setDirection(val: DataCubeQuerySortDirection) { + this.direction = val; + this.onChange?.(); + } +} diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnsPanelState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnsPanelState.ts index d290a74558..46d3604fb6 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnsPanelState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorColumnsPanelState.ts @@ -16,7 +16,7 @@ import { action, makeObservable, observable, override } from 'mobx'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; -import { _toCol } from '../../core/model/DataCubeColumn.js'; +import { _findCol, _toCol } from '../../core/model/DataCubeColumn.js'; import type { DataCubeQueryEditorPanelState } from './DataCubeEditorPanelState.js'; import { DataCubeEditorColumnSelectorColumnState, @@ -183,12 +183,7 @@ export class DataCubeEditorColumnsPanelState // by each extended column. newSnapshot.data.selectColumns = this.selector.selectedColumns // filter out group-level extended columns since these columns are technically not selectable - .filter( - (col) => - !this._editor.groupExtendColumns.find( - (column) => column.name === col.name, - ), - ) + .filter((col) => !_findCol(this._editor.groupExtendColumns, col.name)) .map(_toCol); } } diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.ts index 600f75f707..0910897f62 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.ts @@ -18,28 +18,32 @@ import { action, computed, makeObservable, observable } from 'mobx'; import type { DataCubeConfiguration } from '../../core/model/DataCubeConfiguration.js'; import { DataCubeColumnKind, + DataCubeQuerySortDirection, isPivotResultColumnName, } from '../../core/DataCubeQueryEngine.js'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; import { + _findCol, _toCol, type DataCubeColumn, } from '../../core/model/DataCubeColumn.js'; import { - DataCubeEditorColumnSelectorColumnState, DataCubeEditorColumnSelectorState, + DataCubeEditorSortColumnState, } from './DataCubeEditorColumnSelectorState.js'; import type { DataCubeQueryEditorPanelState } from './DataCubeEditorPanelState.js'; import type { DataCubeEditorState } from './DataCubeEditorState.js'; import { uniqBy } from '@finos/legend-shared'; -export class DataCubeEditorHorizontalPivotColumnSelectorState extends DataCubeEditorColumnSelectorState { +export class DataCubeEditorHorizontalPivotColumnSelectorState extends DataCubeEditorColumnSelectorState { override cloneColumn( - column: DataCubeEditorColumnSelectorColumnState, - ): DataCubeEditorColumnSelectorColumnState { - return new DataCubeEditorColumnSelectorColumnState( + column: DataCubeEditorSortColumnState, + ): DataCubeEditorSortColumnState { + return new DataCubeEditorSortColumnState( column.name, column.type, + column.direction, + column.onChange, ); } @@ -49,13 +53,16 @@ export class DataCubeEditorHorizontalPivotColumnSelectorState extends DataCubeEd (col) => col.kind === DataCubeColumnKind.DIMENSION && // exclude group-level extended columns - !this._editor.groupExtendColumns.find( - (column) => column.name === col.name, - ), + !_findCol(this._editor.groupExtendColumns, col.name), ) .map( (col) => - new DataCubeEditorColumnSelectorColumnState(col.name, col.type), + new DataCubeEditorSortColumnState( + col.name, + col.type, + DataCubeQuerySortDirection.ASCENDING, + () => this.onChange?.(this), + ), ); } } @@ -81,6 +88,16 @@ export class DataCubeEditorHorizontalPivotsPanelState this._editor = editor; this.selector = new DataCubeEditorHorizontalPivotColumnSelectorState( editor, + { + onChange: (selector) => { + // update selection config in column configurations + this._editor.columnProperties.columns.forEach((col) => + col.setPivotSortDirection( + _findCol(selector.selectedColumns, col.name)?.direction, + ), + ); + }, + }, ); } @@ -106,7 +123,13 @@ export class DataCubeEditorHorizontalPivotsPanelState this.selector.setSelectedColumns( (snapshot.data.pivot?.columns ?? []).map( (col) => - new DataCubeEditorColumnSelectorColumnState(col.name, col.type), + new DataCubeEditorSortColumnState( + col.name, + col.type, + _findCol(configuration.columns, col.name)?.pivotSortDirection ?? + DataCubeQuerySortDirection.ASCENDING, + () => this.selector.onChange?.(this.selector), + ), ), ); this.setCastColumns(snapshot.data.pivot?.castColumns ?? []); diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorMutableConfiguration.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorMutableConfiguration.ts index e94db2ec35..99137c20d8 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorMutableConfiguration.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorMutableConfiguration.ts @@ -15,8 +15,9 @@ */ import { - type DataCubeColumnKind, getDataType, + getAggregateOperation, + type DataCubeColumnKind, type DataCubeFont, type DataCubeOperationValue, type DataCubeNumberScale, @@ -46,12 +47,8 @@ import { DataCubeConfiguration, DataCubePivotLayoutConfiguration, } from '../../core/model/DataCubeConfiguration.js'; -import { buildDefaultColumnConfiguration } from '../../core/DataCubeConfigurationBuilder.js'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; -import { - getAggregateOperation, - type DataCubeQueryAggregateOperation, -} from '../../core/aggregation/DataCubeQueryAggregateOperation.js'; +import { type DataCubeQueryAggregateOperation } from '../../core/aggregation/DataCubeQueryAggregateOperation.js'; export class DataCubeEditorMutableColumnConfiguration extends DataCubeColumnConfiguration { readonly dataType!: DataCubeColumnDataType; @@ -197,19 +194,6 @@ export class DataCubeEditorMutableColumnConfiguration extends DataCubeColumnConf return configuration; } - static createDefault( - column: { name: string; type: string }, - aggregateOperations: DataCubeQueryAggregateOperation[], - ) { - return DataCubeEditorMutableColumnConfiguration.create( - DataCubeColumnConfiguration.serialization.toJson( - buildDefaultColumnConfiguration(column), - ), - undefined, - aggregateOperations, - ); - } - get isUsingDefaultStyling() { return ( this.fontFamily === undefined && @@ -393,7 +377,7 @@ export class DataCubeEditorMutableColumnConfiguration extends DataCubeColumnConf this.excludedFromPivot = value; } - setPivotSortDirection(value: string | undefined) { + setPivotSortDirection(value: DataCubeQuerySortDirection | undefined) { this.pivotSortDirection = value; } diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorSortsPanelState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorSortsPanelState.ts index 8e48014708..f8350ff1c3 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorSortsPanelState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorSortsPanelState.ts @@ -14,45 +14,21 @@ * limitations under the License. */ -import { action, makeObservable, observable } from 'mobx'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; -import { _toCol } from '../../core/model/DataCubeColumn.js'; +import { _findCol, _toCol } from '../../core/model/DataCubeColumn.js'; import { DataCubeColumnKind, DataCubeQuerySortDirection, } from '../../core/DataCubeQueryEngine.js'; import type { DataCubeQueryEditorPanelState } from './DataCubeEditorPanelState.js'; import { - DataCubeEditorColumnSelectorColumnState, DataCubeEditorColumnSelectorState, + DataCubeEditorSortColumnState, } from './DataCubeEditorColumnSelectorState.js'; import type { DataCubeEditorState } from './DataCubeEditorState.js'; import type { DataCubeConfiguration } from '../../core/model/DataCubeConfiguration.js'; import { uniqBy } from '@finos/legend-shared'; -export class DataCubeEditorSortColumnState extends DataCubeEditorColumnSelectorColumnState { - direction: DataCubeQuerySortDirection; - - constructor( - name: string, - type: string, - direction: DataCubeQuerySortDirection, - ) { - super(name, type); - - makeObservable(this, { - direction: observable, - setDirection: action, - }); - - this.direction = direction; - } - - setDirection(val: DataCubeQuerySortDirection) { - this.direction = val; - } -} - export class DataCubeEditorSortColumnSelectorState extends DataCubeEditorColumnSelectorState { override cloneColumn(column: DataCubeEditorSortColumnState) { return new DataCubeEditorSortColumnState( @@ -78,8 +54,9 @@ export class DataCubeEditorSortColumnSelectorState extends DataCubeEditorColumnS this._editor.columnProperties.getColumnConfiguration( column.name, ).kind === DataCubeColumnKind.DIMENSION && - !this._editor.horizontalPivots.selector.selectedColumns.find( - (col) => col.name === column.name, + !_findCol( + this._editor.horizontalPivots.selector.selectedColumns, + column.name, ), ), ] @@ -113,7 +90,7 @@ export class DataCubeEditorSortsPanelState adaptPropagatedChanges(): void { this.selector.setSelectedColumns( this.selector.selectedColumns.filter((column) => - this.selector.availableColumns.find((col) => col.name === column.name), + _findCol(this.selector.availableColumns, column.name), ), ); } diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorState.tsx b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorState.tsx index 11e6d7fff0..e4e9d63145 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorState.tsx +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorState.tsx @@ -23,6 +23,7 @@ import { type DataCubeQuerySnapshotExtendedColumn, } from '../../core/DataCubeQuerySnapshot.js'; import { + _findCol, _toCol, type DataCubeColumn, } from '../../core/model/DataCubeColumn.js'; @@ -177,9 +178,7 @@ export class DataCubeEditorState extends DataCubeQuerySnapshotController { this.sorts.selector.availableColumns .filter( (col) => - !tempSnapshot.data.groupExtendedColumns.find( - (column) => column.name === col.name, - ), + !_findCol(tempSnapshot.data.groupExtendedColumns, col.name), ) .map(_toCol); } diff --git a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.ts b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.ts index 86991ab343..223f91e266 100644 --- a/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.ts +++ b/packages/legend-data-cube/src/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.ts @@ -18,7 +18,7 @@ import { uniqBy } from '@finos/legend-shared'; import type { DataCubeConfiguration } from '../../core/model/DataCubeConfiguration.js'; import { DataCubeColumnKind } from '../../core/DataCubeQueryEngine.js'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; -import { _toCol } from '../../core/model/DataCubeColumn.js'; +import { _findCol, _toCol } from '../../core/model/DataCubeColumn.js'; import { DataCubeEditorColumnSelectorColumnState, DataCubeEditorColumnSelectorState, @@ -42,12 +42,11 @@ export class DataCubeEditorVerticalPivotColumnSelectorState extends DataCubeEdit (column) => column.kind === DataCubeColumnKind.DIMENSION && // exclude group-level extended columns - !this._editor.groupExtendColumns.find( - (col) => col.name === column.name, - ) && + !_findCol(this._editor.groupExtendColumns, column.name) && // exclude pivot columns - !this._editor.horizontalPivots.selector.selectedColumns.find( - (col) => col.name === column.name, + !_findCol( + this._editor.horizontalPivots.selector.selectedColumns, + column.name, ), ) .map( @@ -69,7 +68,7 @@ export class DataCubeEditorVerticalPivotsPanelState adaptPropagatedChanges(): void { this.selector.setSelectedColumns( this.selector.selectedColumns.filter((column) => - this.selector.availableColumns.find((col) => col.name === column.name), + _findCol(this.selector.availableColumns, column.name), ), ); } diff --git a/packages/legend-data-cube/src/stores/view/extend/DataCubeColumnEditorState.tsx b/packages/legend-data-cube/src/stores/view/extend/DataCubeColumnEditorState.tsx index 1f46ef4198..04c2a1e899 100644 --- a/packages/legend-data-cube/src/stores/view/extend/DataCubeColumnEditorState.tsx +++ b/packages/legend-data-cube/src/stores/view/extend/DataCubeColumnEditorState.tsx @@ -52,6 +52,7 @@ import { _serializeValueSpecification, _deserializeValueSpecification, } from '../../core/DataCubeQueryBuilderUtils.js'; +import { _findCol } from '../../core/model/DataCubeColumn.js'; export abstract class DataCubeColumnBaseEditorState { protected readonly uuid = uuid(); @@ -244,9 +245,7 @@ export abstract class DataCubeColumnBaseEditorState { this.buildExtendBaseQuery(), this.view.source, ); - let returnType = returnRelationType.columns.find( - (col) => col.name === this._name, - )?.type; + let returnType = _findCol(returnRelationType.columns, this._name)?.type; returnType = returnType && isPrimitiveType(returnType) ? returnType : undefined; this.setReturnType(returnType); diff --git a/packages/legend-data-cube/src/stores/view/extend/DataCubeExtendManagerState.tsx b/packages/legend-data-cube/src/stores/view/extend/DataCubeExtendManagerState.tsx index 4e308af468..15dbf5ec80 100644 --- a/packages/legend-data-cube/src/stores/view/extend/DataCubeExtendManagerState.tsx +++ b/packages/legend-data-cube/src/stores/view/extend/DataCubeExtendManagerState.tsx @@ -20,6 +20,7 @@ import { type DataCubeQuerySnapshotExtendedColumn, } from '../../core/DataCubeQuerySnapshot.js'; import { + _findCol, _toCol, type DataCubeColumn, } from '../../core/model/DataCubeColumn.js'; @@ -45,7 +46,7 @@ import { DataCubeColumnKind, getDataType, } from '../../core/DataCubeQueryEngine.js'; -import { buildDefaultColumnConfiguration } from '../../core/DataCubeConfigurationBuilder.js'; +import { newColumnConfiguration } from '../../core/DataCubeConfigurationBuilder.js'; import { _lambda } from '../../core/DataCubeQueryBuilderUtils.js'; import { EngineError } from '@finos/legend-graph'; @@ -142,21 +143,21 @@ export class DataCubeExtendManagerState extends DataCubeQuerySnapshotController } if ( - !this.leafExtendedColumns.find((col) => col.name === columnName) && - !this.groupExtendedColumns.find((col) => col.name === columnName) + !_findCol(this.leafExtendedColumns, columnName) && + !_findCol(this.groupExtendedColumns, columnName) ) { return; } const editor = new DataCubeExistingColumnEditorState( this, guaranteeNonNullable( - this.leafExtendedColumns.find((col) => col.name === columnName) ?? - this.groupExtendedColumns.find((col) => col.name === columnName), + _findCol(this.leafExtendedColumns, columnName) ?? + _findCol(this.groupExtendedColumns, columnName), ).data, guaranteeNonNullable( - this.columnConfigurations.find((col) => col.name === columnName), + _findCol(this.columnConfigurations, columnName), ).kind, - Boolean(this.groupExtendedColumns.find((col) => col.name === columnName)), + Boolean(_findCol(this.groupExtendedColumns, columnName)), ); await editor.initialize(); this.existingColumnEditors.push(editor); @@ -169,7 +170,7 @@ export class DataCubeExtendManagerState extends DataCubeQuerySnapshotController columnKind: DataCubeColumnKind | undefined, editor: DataCubeNewColumnState, ) { - const columnConfiguration = buildDefaultColumnConfiguration(column); + const columnConfiguration = newColumnConfiguration(column); if (columnKind) { columnConfiguration.kind = columnKind; columnConfiguration.excludedFromPivot = @@ -197,14 +198,14 @@ export class DataCubeExtendManagerState extends DataCubeQuerySnapshotController columnKind: DataCubeColumnKind | undefined, ) { if ( - !this.leafExtendedColumns.find((col) => col.name === columnName) && - !this.groupExtendedColumns.find((col) => col.name === columnName) + !_findCol(this.leafExtendedColumns, columnName) && + !_findCol(this.groupExtendedColumns, columnName) ) { return; } const columnConfiguration = guaranteeNonNullable( - this.columnConfigurations.find((col) => col.name === columnName), + _findCol(this.columnConfigurations, columnName), ); const task = this.view.taskService.newTask('Column update check'); @@ -326,8 +327,8 @@ export class DataCubeExtendManagerState extends DataCubeQuerySnapshotController async deleteColumn(columnName: string) { if ( - !this.leafExtendedColumns.find((col) => col.name === columnName) && - !this.groupExtendedColumns.find((col) => col.name === columnName) + !_findCol(this.leafExtendedColumns, columnName) && + !_findCol(this.groupExtendedColumns, columnName) ) { return; } diff --git a/packages/legend-data-cube/src/stores/view/filter/DataCubeFilterEditorState.tsx b/packages/legend-data-cube/src/stores/view/filter/DataCubeFilterEditorState.tsx index 421d4ec5b1..38f9dcc39a 100644 --- a/packages/legend-data-cube/src/stores/view/filter/DataCubeFilterEditorState.tsx +++ b/packages/legend-data-cube/src/stores/view/filter/DataCubeFilterEditorState.tsx @@ -37,6 +37,7 @@ import { } from '../../core/model/DataCubeConfiguration.js'; import type { DisplayState } from '../../services/DataCubeLayoutService.js'; import { DataCubeFilterEditor } from '../../../components/view/filter/DataCubeFilterEditor.js'; +import { _findCol } from '../../core/model/DataCubeColumn.js'; /** * This query editor state backs the form editor for filter. It batches changes made @@ -295,10 +296,7 @@ export class DataCubeFilterEditorState extends DataCubeQuerySnapshotController { ); // NOTE: filtering group-level extended columns is not supported this.columns = configuration.columns.filter( - (column) => - !snapshot.data.groupExtendedColumns.find( - (col) => col.name === column.name, - ), + (column) => !_findCol(snapshot.data.groupExtendedColumns, column.name), ); this.tree.nodes = new Map(); diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridClientEngine.ts b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridClientEngine.ts index d6ab244517..ab9768b052 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridClientEngine.ts +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridClientEngine.ts @@ -31,18 +31,19 @@ import { pruneObject, } from '@finos/legend-shared'; import { buildExecutableQuery } from '../../core/DataCubeQueryBuilder.js'; -import { type TabularDataSet, V1_Lambda } from '@finos/legend-graph'; +import { + PureClientVersion, + type TabularDataSet, + V1_Lambda, +} from '@finos/legend-graph'; import { makeObservable, observable, runInAction } from 'mobx'; import type { DataCubeConfiguration, DataCubeConfigurationColorKey, } from '../../core/model/DataCubeConfiguration.js'; import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; -import { _sortByColName } from '../../core/model/DataCubeColumn.js'; -import { - isPivotResultColumnName, - type DataCubeQueryFunctionMap, -} from '../../core/DataCubeQueryEngine.js'; +import { _findCol, _sortByColName } from '../../core/model/DataCubeColumn.js'; +import { isPivotResultColumnName } from '../../core/DataCubeQueryEngine.js'; import { buildQuerySnapshot } from './DataCubeGridQuerySnapshotBuilder.js'; import { AlertType } from '../../services/DataCubeAlertService.js'; import { sum } from 'mathjs'; @@ -260,6 +261,7 @@ async function getCastColumns( const snapshot = currentSnapshot.clone(); guaranteeNonNullable(snapshot.data.pivot).castColumns = []; snapshot.data.groupBy = undefined; + snapshot.data.groupExtendedColumns = []; snapshot.data.sortColumns = []; snapshot.data.limit = 0; const query = buildExecutableQuery( @@ -277,18 +279,6 @@ async function getCastColumns( filterOperations, aggregateOperations, ) => { - const _unprocess = (funcMapKey: keyof DataCubeQueryFunctionMap) => { - const func = funcMap[funcMapKey]; - if (func) { - sequence.splice(sequence.indexOf(func), 1); - funcMap[funcMapKey] = undefined; - } - }; - - if (funcMap.groupExtend) { - _unprocess('groupExtend'); - } - // when both pivot and groupBy present, we need to account for the count column // in the cast expression if (funcMap.pivot && currentSnapshot.data.groupBy) { @@ -304,6 +294,11 @@ async function getCastColumns( debug: view.settingService.getBooleanValue( DataCubeSettingKey.DEBUGGER__ENABLE_DEBUG_MODE, ), + clientVersion: view.settingService.getBooleanValue( + DataCubeSettingKey.DEBUGGER__USE_DEV_CLIENT_PROTOCOL_VERSION, + ) + ? PureClientVersion.VX_X_X + : undefined, }); return result.result.builder.columns.map((column) => ({ @@ -380,8 +375,9 @@ export class DataCubeGridClientServerSideDataSource newSnapshot.data.pivot.castColumns = castColumns; newSnapshot.data.sortColumns = newSnapshot.data.sortColumns.filter( (column) => - [...castColumns, ...newSnapshot.data.groupExtendedColumns].find( - (col) => column.name === col.name, + _findCol( + [...castColumns, ...newSnapshot.data.groupExtendedColumns], + column.name, ), ); } catch (error) { @@ -430,6 +426,11 @@ export class DataCubeGridClientServerSideDataSource debug: this._view.settingService.getBooleanValue( DataCubeSettingKey.DEBUGGER__ENABLE_DEBUG_MODE, ), + clientVersion: this._view.settingService.getBooleanValue( + DataCubeSettingKey.DEBUGGER__USE_DEV_CLIENT_PROTOCOL_VERSION, + ) + ? PureClientVersion.VX_X_X + : undefined, }, ); const rowData = buildRowData(result.result.result, newSnapshot); diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridConfigurationBuilder.tsx b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridConfigurationBuilder.tsx index 25e58d9b64..7f128af935 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridConfigurationBuilder.tsx +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridConfigurationBuilder.tsx @@ -21,9 +21,13 @@ * AG Grid, from the query snapshot. ***************************************************************************************/ -import { type DataCubeQuerySnapshot } from '../../core/DataCubeQuerySnapshot.js'; +import { + type DataCubeQuerySnapshot, + type DataCubeQuerySnapshotPivot, +} from '../../core/DataCubeQuerySnapshot.js'; import { _findCol, + _toCol, type DataCubeColumn, } from '../../core/model/DataCubeColumn.js'; import type { @@ -61,11 +65,11 @@ import { last, getQueryParameters, getQueryParameterValue, - guaranteeNonNullable, isNonNullable, isNullable, isNumber, isValidUrl, + assertTrue, } from '@finos/legend-shared'; import type { DataCubeColumnConfiguration, @@ -216,17 +220,15 @@ function _displaySpec(columnData: ColumnData) { column.hideFromView || !column.isSelected || (Boolean( - snapshot.data.pivot && - !snapshot.data.pivot.castColumns.find((col) => col.name === name), + snapshot.data.pivot && !_findCol(snapshot.data.pivot.castColumns, name), ) && - !snapshot.data.groupExtendedColumns.find((col) => col.name === name)), + !_findCol(snapshot.data.groupExtendedColumns, name)), lockVisible: !column.isSelected || (Boolean( - snapshot.data.pivot && - !snapshot.data.pivot.castColumns.find((col) => col.name === name), + snapshot.data.pivot && !_findCol(snapshot.data.pivot.castColumns, name), ) && - !snapshot.data.groupExtendedColumns.find((col) => col.name === name)), + !_findCol(snapshot.data.groupExtendedColumns, name)), pinned: column.pinned !== undefined ? column.pinned === DataCubeColumnPinPlacement.RIGHT @@ -643,7 +645,7 @@ function generatePivotResultColumnHeaderTooltip( return values.join(' / '); } if (values.length === snapshot.data.pivot.columns.length + 1) { - const baseColumnName = guaranteeNonNullable(values[values.length - 1]); + const baseColumnName = at(values, values.length - 1); const columnConfiguration = _findCol(configuration.columns, baseColumnName); return `Column = ${ columnConfiguration @@ -783,6 +785,53 @@ function generateDefinitionForPivotResultColumns( return columnDefs; } +/** + * We tried to push-down to the DB level to ensure a particular order + * for the pivot result columns, we do so by preceding pivot() with a sort(). + * + * Implementations of pivot() is highly non-standard across different DBs, so + * this rearranging needs to be done client-side. + */ +function rearrangePivotResultColumns( + pivotResultColumns: DataCubeColumn[], + pivotData: DataCubeQuerySnapshotPivot, + configuration: DataCubeConfiguration, +) { + try { + const columns: (DataCubeColumn & { values: string[] })[] = []; + for (const pivotResultColumn of pivotResultColumns) { + const column = { + ...pivotResultColumn, + values: pivotResultColumn.name + .split(PIVOT_COLUMN_NAME_VALUE_SEPARATOR) + .slice(0, -1), // remove the last entry + }; + assertTrue(column.values.length === pivotData.columns.length); + columns.push(column); + } + const columnConfigs = pivotData.columns + .map((col) => configuration.getColumn(col.name)) + .filter(isNonNullable); + assertTrue(columnConfigs.length === pivotData.columns.length); + + // apply multi dimensional sorts by starting from the last pivot column to the first + for (let i = pivotData.columns.length - 1; i >= 0; i--) { + const direction = + columnConfigs[i]?.pivotSortDirection ?? + DataCubeQuerySortDirection.ASCENDING; + columns.sort((a, b) => + direction === DataCubeQuerySortDirection.ASCENDING + ? at(a.values, i).localeCompare(at(b.values, i)) + : at(b.values, i).localeCompare(at(a.values, i)), + ); + } + + return columns.map((col) => _toCol(col)); + } catch { + return pivotResultColumns; + } +} + export function generateColumnDefs( snapshot: DataCubeQuerySnapshot, configuration: DataCubeConfiguration, @@ -795,17 +844,17 @@ export function generateColumnDefs( // must still be included in the column definitions, and made hidden instead. const columns = configuration.columns.filter( (col) => - snapshot.data.selectColumns.find((column) => column.name === col.name) ?? - snapshot.data.groupExtendedColumns.find( - (column) => column.name === col.name, - ), + _findCol(snapshot.data.selectColumns, col.name) ?? + _findCol(snapshot.data.groupExtendedColumns, col.name), ); let pivotResultColumns: DataCubeColumn[] = []; if (snapshot.data.pivot) { const castColumns = snapshot.data.pivot.castColumns; - pivotResultColumns = castColumns.filter((col) => - isPivotResultColumnName(col.name), + pivotResultColumns = rearrangePivotResultColumns( + castColumns.filter((col) => isPivotResultColumnName(col.name)), + snapshot.data.pivot, + configuration, ); } diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridControllerState.ts b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridControllerState.ts index ef194058fa..d765297d70 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridControllerState.ts +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridControllerState.ts @@ -26,15 +26,16 @@ import { type DataCubeQuerySnapshotSortColumn, } from '../../core/DataCubeQuerySnapshot.js'; import { + _findCol, _toCol, type DataCubeColumn, } from '../../core/model/DataCubeColumn.js'; import { DataCubeQuerySnapshotController } from '../../services/DataCubeQuerySnapshotService.js'; import { - type DataCubeQuerySortDirection, type DataCubeColumnPinPlacement, DataCubeColumnKind, DataCubeQueryFilterGroupOperator, + DataCubeQuerySortDirection, isPivotResultColumnName, getPivotResultColumnBaseColumnName, } from '../../core/DataCubeQueryEngine.js'; @@ -92,7 +93,7 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController | undefined; getColumnConfiguration(colName: string | undefined) { - return this.configuration.columns.find((col) => col.name === colName); + return _findCol(this.configuration.columns, colName); } // --------------------------------- FILTER --------------------------------- @@ -184,7 +185,7 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController ]; const rearrangedSelectColumns = columns - .map((colName) => this.selectColumns.find((col) => col.name === colName)) + .map((colName) => _findCol(this.selectColumns, colName)) .filter(isNonNullable); this.selectColumns = [ ...rearrangedSelectColumns, @@ -221,14 +222,15 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController } getHorizontalPivotableColumn(colName: string) { - return this.configuration.columns - .filter( + return _findCol( + this.configuration.columns.filter( (col) => col.kind === DataCubeColumnKind.DIMENSION && // exclude group-level extended columns - !this.groupExtendedColumns.find((column) => column.name === col.name), - ) - .find((col) => col.name === colName); + !_findCol(this.groupExtendedColumns, col.name), + ), + colName, + ); } setHorizontalPivotOnColumn(colName: string) { @@ -277,20 +279,17 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController verticalPivotColumns: DataCubeColumn[] = []; getVerticalPivotableColumn(colName: string) { - return this.configuration.columns - .filter( + return _findCol( + this.configuration.columns.filter( (col) => col.kind === DataCubeColumnKind.DIMENSION && // exclude group-level extended columns - !this.groupExtendedColumns.find( - (column) => column.name === col.name, - ) && + !_findCol(this.groupExtendedColumns, col.name) && // exclude pivot columns - !this.horizontalPivotColumns.find( - (column) => column.name === col.name, - ), - ) - .find((col) => col.name === colName); + !_findCol(this.horizontalPivotColumns, col.name), + ), + colName, + ); } setVerticalPivotOnColumn(colName: string) { @@ -349,12 +348,15 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController if (!colName) { return undefined; } - return [ - ...(this.horizontalPivotCastColumns.length - ? this.horizontalPivotCastColumns - : this.selectColumns), - ...this.groupExtendedColumns, - ].find((col) => col.name === colName); + return _findCol( + [ + ...(this.horizontalPivotCastColumns.length + ? this.horizontalPivotCastColumns + : this.selectColumns), + ...this.groupExtendedColumns, + ], + colName, + ); } setSortByColumn(colName: string, direction: DataCubeQuerySortDirection) { @@ -430,23 +432,24 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController private propagateChanges(baseSnapshot: DataCubeQuerySnapshot) { this.verticalPivotColumns = this.verticalPivotColumns.filter( - (col) => - !this.horizontalPivotColumns.find((column) => column.name === col.name), + (col) => !_findCol(this.horizontalPivotColumns, col.name), ); this.configuration.pivotLayout.expandedPaths = _pruneExpandedPaths( baseSnapshot.data.groupBy?.columns ?? [], this.verticalPivotColumns, this.configuration.pivotLayout.expandedPaths, ); + this.configuration.columns.forEach((col) => { + col.pivotSortDirection = _findCol(this.horizontalPivotColumns, col.name) + ? (col.pivotSortDirection ?? DataCubeQuerySortDirection.ASCENDING) + : undefined; + }); this.selectColumns = uniqBy( [ ...this.configuration.columns.filter( (col) => - col.isSelected && - !this.groupExtendedColumns.find( - (column) => column.name === col.name, - ), + col.isSelected && !_findCol(this.groupExtendedColumns, col.name), ), ...this.horizontalPivotColumns, ...this.verticalPivotColumns, @@ -468,9 +471,7 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController (column) => this.getColumnConfiguration(column.name)?.kind === DataCubeColumnKind.DIMENSION && - !this.horizontalPivotColumns.find( - (col) => col.name === column.name, - ), + !_findCol(this.horizontalPivotColumns, column.name), ), ] : [ @@ -482,7 +483,7 @@ export class DataCubeGridControllerState extends DataCubeQuerySnapshotController (col) => col.name, ); this.sortColumns = this.sortColumns.filter((col) => - sortableColumns.find((column) => column.name === col.name), + _findCol(sortableColumns, col.name), ); } diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridMenuBuilder.tsx b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridMenuBuilder.tsx index 39c21e3c5a..9ba5579673 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridMenuBuilder.tsx +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridMenuBuilder.tsx @@ -47,6 +47,7 @@ import { PRIMITIVE_TYPE } from '@finos/legend-graph'; import type { DataCubeColumnConfiguration } from '../../core/model/DataCubeConfiguration.js'; import { DataCubeFilterEditorConditionTreeNode } from '../../core/filter/DataCubeQueryFilterEditorState.js'; import { DataCubeEditorTab } from '../editor/DataCubeEditorState.js'; +import { _findCol } from '../../core/model/DataCubeColumn.js'; function toFilterValue( value: unknown, @@ -189,10 +190,10 @@ export function generateMenuBuilder( : undefined; const isExtendedColumn = columnName && - [ - ...controller.leafExtendedColumns, - ...controller.groupExtendedColumns, - ].find((col) => col.name === columnName); + _findCol( + [...controller.leafExtendedColumns, ...controller.groupExtendedColumns], + columnName, + ); // NOTE: here we assume the value must be coming from the same column const value: unknown = 'value' in params ? params.value : undefined; @@ -220,9 +221,7 @@ export function generateMenuBuilder( }, { name: 'Clear Sort', - disabled: !controller.sortColumns.find( - (col) => col.name === columnName, - ), + disabled: !_findCol(controller.sortColumns, columnName), action: () => controller.clearSortByColumn(columnName), }, 'separator', @@ -513,16 +512,15 @@ export function generateMenuBuilder( { name: `Add Vertical Pivot on ${columnName}`, disabled: Boolean( - controller.verticalPivotColumns.find( - (col) => col.name === columnName, - ), + _findCol(controller.verticalPivotColumns, columnName), ), action: () => controller.addVerticalPivotOnColumn(columnName), }, { name: `Remove Vertical Pivot on ${columnName}`, - disabled: !controller.verticalPivotColumns.find( - (col) => col.name === columnName, + disabled: !_findCol( + controller.verticalPivotColumns, + columnName, ), action: () => controller.removeVerticalPivotOnColumn(columnName), @@ -543,9 +541,7 @@ export function generateMenuBuilder( { name: `Add Horizontal Pivot on ${columnName}`, disabled: Boolean( - controller.horizontalPivotColumns.find( - (col) => col.name === columnName, - ), + _findCol(controller.horizontalPivotColumns, columnName), ), action: () => controller.addHorizontalPivotOnColumn(columnName), @@ -696,10 +692,9 @@ export function generateMenuBuilder( ...controller.horizontalPivotCastColumns .map((col) => { if (isPivotResultColumnName(col.name)) { - const colConf = controller.configuration.columns.find( - (c) => - c.name === - getPivotResultColumnBaseColumnName(col.name), + const colConf = _findCol( + controller.configuration.columns, + getPivotResultColumnBaseColumnName(col.name), ); if ( colConf && @@ -729,10 +724,9 @@ export function generateMenuBuilder( ...controller.horizontalPivotCastColumns .map((col) => { if (isPivotResultColumnName(col.name)) { - const colConf = controller.configuration.columns.find( - (c) => - c.name === - getPivotResultColumnBaseColumnName(col.name), + const colConf = _findCol( + controller.configuration.columns, + getPivotResultColumnBaseColumnName(col.name), ); if ( colConf && diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQueryBuilder.ts b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQueryBuilder.ts index 090dcaa312..a6f51c4f55 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQueryBuilder.ts +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQueryBuilder.ts @@ -90,7 +90,7 @@ function _aggCountCastCol(colName: string) { const variable = _var(); return _colSpec( colName, - _lambda([variable], [_property(colName, variable)]), + _lambda([variable], [_property(colName)]), _lambda([variable], [_function(DataCubeFunction.SUM, [variable])]), ); } diff --git a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQuerySnapshotBuilder.ts b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQuerySnapshotBuilder.ts index 3bd7a83cdb..54d4b77a58 100644 --- a/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQuerySnapshotBuilder.ts +++ b/packages/legend-data-cube/src/stores/view/grid/DataCubeGridQuerySnapshotBuilder.ts @@ -43,7 +43,7 @@ export function getColumnConfiguration( configuration: DataCubeConfiguration, ) { return guaranteeNonNullable( - configuration.columns.find((col) => col.name === colName), + configuration.getColumn(colName), `Can't find configuration for column '${colName}'`, ); } diff --git a/packages/legend-data-cube/style/index.scss b/packages/legend-data-cube/style/index.scss index a61590b40d..4210180d3b 100644 --- a/packages/legend-data-cube/style/index.scss +++ b/packages/legend-data-cube/style/index.scss @@ -83,6 +83,18 @@ width: 100%; } + .markdown-content p, + .markdown-content blockquote, + .markdown-content ul, + .markdown-content ol, + .markdown-content dl, + .markdown-content table, + .markdown-content pre, + .markdown-content details { + margin-top: 0; + margin-bottom: 10px; + } + .markdown-content [hidden] { display: none !important; } @@ -169,6 +181,12 @@ border: 0; font-size: 12px; } + + .markdown-content blockquote { + padding: 0 10px; + color: var(--tw-color-neutral-500); + border-left: 3px solid var(--tw-color-neutral-200); + } } // ---------------------------------------- CUSTOMIZE ---------------------------------------- diff --git a/packages/legend-graph/src/graph-manager/__test-utils__/EngineTestSupport.ts b/packages/legend-graph/src/graph-manager/__test-utils__/EngineTestSupport.ts index 6a9b6a449e..66ba11e999 100644 --- a/packages/legend-graph/src/graph-manager/__test-utils__/EngineTestSupport.ts +++ b/packages/legend-graph/src/graph-manager/__test-utils__/EngineTestSupport.ts @@ -19,7 +19,7 @@ import type { ClassifierPathMapping, SubtypeInfo, } from '../action/protocol/ProtocolInfo.js'; -import axios, { type AxiosResponse } from 'axios'; +import axios, { type AxiosResponse, AxiosError } from 'axios'; import { ContentType, HttpHeader, @@ -33,14 +33,18 @@ import { V1_ArtifactGenerationExtensionInput, type V1_ArtifactGenerationExtensionOutput, } from '../protocol/pure/v1/engine/generation/V1_ArtifactGenerationExtensionApi.js'; +import type { V1_Lambda } from '../protocol/pure/v1/model/valueSpecification/raw/V1_Lambda.js'; +import type { V1_RelationType } from '../protocol/pure/v1/model/packageableElements/type/V1_RelationType.js'; export const ENGINE_TEST_SUPPORT_API_URL = 'http://localhost:6300/api'; +export { AxiosError as ENGINE_TEST_SUPPORT__NetworkClientError }; + export async function ENGINE_TEST_SUPPORT__getClassifierPathMapping(): Promise< ClassifierPathMapping[] > { return ( - await axios.get>( + await axios.get( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/protocol/pure/getClassifierPathMap`, ) ).data; @@ -58,7 +62,7 @@ export async function ENGINE_TEST_SUPPORT__execute( executionInput: V1_ExecuteInput, ): Promise> { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/execution/execute`, V1_ExecuteInput.serialization.toJson(executionInput), { @@ -75,7 +79,7 @@ export async function ENGINE_TEST_SUPPORT__grammarToJSON_model( returnSourceInformation?: boolean | undefined, ): Promise<{ elements: object[] }> { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/grammar/grammarToJson/model`, code, { @@ -95,7 +99,7 @@ export async function ENGINE_TEST_SUPPORT__JSONToGrammar_valueSpecification( pretty?: boolean | undefined, ): Promise { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/grammar/jsonToGrammar/valueSpecification`, value, { @@ -116,7 +120,7 @@ export async function ENGINE_TEST_SUPPORT__grammarToJSON_valueSpecification( returnSourceInformation?: boolean | undefined, ): Promise> { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/grammar/grammarToJson/valueSpecification`, code, { @@ -136,7 +140,7 @@ export async function ENGINE_TEST_SUPPORT__JSONToGrammar_model( pretty?: boolean | undefined, ): Promise { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/grammar/jsonToGrammar/model`, model, { @@ -154,17 +158,32 @@ export async function ENGINE_TEST_SUPPORT__JSONToGrammar_model( export async function ENGINE_TEST_SUPPORT__compile( model: PlainObject, ): Promise> { - return axios.post>( + return axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/compilation/compile`, model, ); } +export async function ENGINE_TEST_SUPPORT__getLambdaRelationType( + lambda: PlainObject, + model: PlainObject, +): Promise> { + return ( + await axios.post( + `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/compilation/lambdaRelationType`, + { + lambda, + model, + }, + ) + ).data; +} + export async function ENGINE_TEST_SUPPORT__generateArtifacts( input: V1_ArtifactGenerationExtensionInput, ): Promise> { return ( - await axios.post>( + await axios.post( `${ENGINE_TEST_SUPPORT_API_URL}/pure/v1/generation/generateArtifacts`, V1_ArtifactGenerationExtensionInput.serialization.toJson(input), ) diff --git a/packages/legend-query-builder/src/stores/data-cube/QueryBuilderDataCubeEngine.ts b/packages/legend-query-builder/src/stores/data-cube/QueryBuilderDataCubeEngine.ts index 8a0f0f0207..8ef4400885 100644 --- a/packages/legend-query-builder/src/stores/data-cube/QueryBuilderDataCubeEngine.ts +++ b/packages/legend-query-builder/src/stores/data-cube/QueryBuilderDataCubeEngine.ts @@ -25,7 +25,6 @@ import { V1_RawLambda, V1_serializeValueSpecification, type GraphManagerState, - type PureModel, type V1_ValueSpecification, type ParameterValue, LAMBDA_PIPE, @@ -79,53 +78,7 @@ export class QueryBuilderDataCubeEngine extends DataCubeEngine { this.parameters = selectQuery.parameters; } - get sourceLabel(): string { - return `Query Builder Report`; - } - - get graph(): PureModel { - return this.graphState.graph; - } - - private getSourceFunctionExpression() { - let srcFuncExp = V1_deserializeValueSpecification( - this.graphState.graphManager.serializeRawValueSpecification( - this.selectInitialQuery, - ), - [], - ); - // We could do a further check here to ensure the experssion is an applied funciton - // this is because data cube expects an expression to be able to built further upon the queery - if ( - srcFuncExp instanceof V1_Lambda && - srcFuncExp.body.length === 1 && - srcFuncExp.body[0] - ) { - srcFuncExp = srcFuncExp.body[0]; - } - return srcFuncExp; - } - - async generateInitialQuery() { - const srcFuncExp = this.getSourceFunctionExpression(); - const fromFuncExp = new V1_AppliedFunction(); - fromFuncExp.function = _functionName(SUPPORTED_FUNCTIONS.FROM); - fromFuncExp.parameters = [srcFuncExp]; - if (this.mappingPath) { - fromFuncExp.parameters.push(_elementPtr(this.mappingPath)); - } - if (this.runtimePath) { - fromFuncExp.parameters.push(_elementPtr(this.runtimePath)); - } - const columns = (await this.getRelationType(this.selectInitialQuery)) - .columns; - const query = new DataCubeQuery(); - query.query = `~[${columns.map((e) => `'${e.name}'`)}]->select()`; - - return query; - } - - async processQuerySource(value: PlainObject) { + override async processQuerySource(value: PlainObject) { // TODO: this is an abnormal usage of this method, this is the place // where we can enforce which source this engine supports, instead // of hardcoding the logic like this. @@ -140,17 +93,30 @@ export class QueryBuilderDataCubeEngine extends DataCubeEngine { return source; } - private buildRawLambdaFromValueSpec(query: V1_Lambda): RawLambda { - const json = guaranteeType( - V1_deserializeRawValueSpecification( - V1_serializeValueSpecification(query, []), + override async parseValueSpecification( + code: string, + returnSourceInformation?: boolean, + ) { + return V1_deserializeValueSpecification( + await this.graphState.graphManager.pureCodeToValueSpecification( + code, + returnSourceInformation, ), - V1_RawLambda, + [], + ); + } + + override async getValueSpecificationCode( + value: V1_ValueSpecification, + pretty?: boolean | undefined, + ) { + return this.graphState.graphManager.valueSpecificationToPureCode( + V1_serializeValueSpecification(value, []), + pretty, ); - return new RawLambda(json.parameters, json.body); } - async getQueryTypeahead( + override async getQueryTypeahead( code: string, baseQuery: V1_Lambda, source: DataCubeSource, @@ -166,42 +132,17 @@ export class QueryBuilderDataCubeEngine extends DataCubeEngine { ); const result = await this.graphState.graphManager.getCodeComplete( finalCode, - this.graph, + this.graphState.graph, offset, ); return result.completions as CompletionItem[]; } - override async parseValueSpecification( - code: string, - returnSourceInformation?: boolean, - ) { - return V1_deserializeValueSpecification( - await this.graphState.graphManager.pureCodeToValueSpecification( - code, - returnSourceInformation, - ), - [], - ); - } - - override async getValueSpecificationCode( - value: V1_ValueSpecification, - pretty?: boolean | undefined, + override async getQueryRelationReturnType( + query: V1_Lambda, + source: DataCubeSource, ) { - return this.graphState.graphManager.valueSpecificationToPureCode( - V1_serializeValueSpecification(value, []), - pretty, - ); - } - - async getRelationType(query: RawLambda) { - const relationType = - await this.graphState.graphManager.getLambdaRelationType( - query, - this.graph, - ); - return relationType; + return this.getRelationType(this.buildRawLambdaFromValueSpec(query)); } override async getQueryCodeRelationReturnType( @@ -227,7 +168,7 @@ export class QueryBuilderDataCubeEngine extends DataCubeEngine { lambda, undefined, undefined, - this.graph, + this.graphState.graph, { parameterValues: this.parameterValues ?? [], }, @@ -268,4 +209,63 @@ export class QueryBuilderDataCubeEngine extends DataCubeEngine { } return undefined; } + + // ---------------------------------- UTILITIES ---------------------------------- + + private buildRawLambdaFromValueSpec(query: V1_Lambda): RawLambda { + const json = guaranteeType( + V1_deserializeRawValueSpecification( + V1_serializeValueSpecification(query, []), + ), + V1_RawLambda, + ); + return new RawLambda(json.parameters, json.body); + } + + private getSourceFunctionExpression() { + let srcFuncExp = V1_deserializeValueSpecification( + this.graphState.graphManager.serializeRawValueSpecification( + this.selectInitialQuery, + ), + [], + ); + // We could do a further check here to ensure the experssion is an applied funciton + // this is because data cube expects an expression to be able to built further upon the queery + if ( + srcFuncExp instanceof V1_Lambda && + srcFuncExp.body.length === 1 && + srcFuncExp.body[0] + ) { + srcFuncExp = srcFuncExp.body[0]; + } + return srcFuncExp; + } + + async generateInitialQuery() { + const srcFuncExp = this.getSourceFunctionExpression(); + const fromFuncExp = new V1_AppliedFunction(); + fromFuncExp.function = _functionName(SUPPORTED_FUNCTIONS.FROM); + fromFuncExp.parameters = [srcFuncExp]; + if (this.mappingPath) { + fromFuncExp.parameters.push(_elementPtr(this.mappingPath)); + } + if (this.runtimePath) { + fromFuncExp.parameters.push(_elementPtr(this.runtimePath)); + } + const columns = (await this.getRelationType(this.selectInitialQuery)) + .columns; + const query = new DataCubeQuery(); + query.query = `~[${columns.map((e) => `'${e.name}'`)}]->select()`; + + return query; + } + + async getRelationType(query: RawLambda) { + const relationType = + await this.graphState.graphManager.getLambdaRelationType( + query, + this.graphState.graph, + ); + return relationType; + } }