From bf5dfbcafb2b3b874fe6f680d38df912ffd35a75 Mon Sep 17 00:00:00 2001 From: Mauricio Uyaguari Date: Thu, 23 Jan 2025 19:33:04 -0500 Subject: [PATCH] fix result modifiers for typed tds (#3834) --- .changeset/weak-pillows-wave.md | 4 + .../ValueSpecificationObserver.ts | 21 ++ .../from/V1_ValueSpecificationTransformer.ts | 28 +- .../V1_ValueSpecificationBuilderHelper.ts | 16 + .../RelationValueSpecification.ts | 16 + .../valueSpecification/ValueSpecification.ts | 6 +- packages/legend-graph/src/index.ts | 1 + ...yRoundtripGrammar.engine-roundtrip-test.ts | 51 +++ .../src/graph/QueryBuilderMetaModelConst.ts | 6 +- .../src/stores/QueryBuilderStateBuilder.ts | 24 +- .../TEST_DATA__QueryBuilder_Generic.ts | 2 +- .../QueryBuilderProjectionStateBuilder.ts | 65 +++- ...lderProjectionValueSpecificationBuilder.ts | 162 +-------- ...ResultModifierValueSpecificationBuilder.ts | 337 ++++++++++++++++++ 14 files changed, 585 insertions(+), 154 deletions(-) create mode 100644 .changeset/weak-pillows-wave.md create mode 100644 packages/legend-query-builder/src/stores/fetch-structure/tds/result-modifier/ResultModifierValueSpecificationBuilder.ts diff --git a/.changeset/weak-pillows-wave.md b/.changeset/weak-pillows-wave.md new file mode 100644 index 0000000000..311767078d --- /dev/null +++ b/.changeset/weak-pillows-wave.md @@ -0,0 +1,4 @@ +--- +'@finos/legend-query-builder': patch +'@finos/legend-graph': patch +--- diff --git a/packages/legend-graph/src/graph-manager/action/changeDetection/ValueSpecificationObserver.ts b/packages/legend-graph/src/graph-manager/action/changeDetection/ValueSpecificationObserver.ts index 102061b786..55bb8f7fd9 100644 --- a/packages/legend-graph/src/graph-manager/action/changeDetection/ValueSpecificationObserver.ts +++ b/packages/legend-graph/src/graph-manager/action/changeDetection/ValueSpecificationObserver.ts @@ -64,6 +64,7 @@ import type { ColSpec, ColSpecArray, ColSpecArrayInstance, + ColSpecInstanceValue, } from '../../../graph/metamodel/pure/valueSpecification/RelationValueSpecification.js'; const observe_Abstract_ValueSpecification = ( @@ -194,6 +195,10 @@ export const observe_ColSpecArrayInstance = skipObservedWithContext( _observe_ColSpecArrayInstance, ); +export const observe_ColSpecInstance = skipObservedWithContext( + _observe_ColSpecInstance, +); + const observe_LambdaFunction = skipObservedWithContext(_observe_LambdaFunction); const observe_LambdaFunctionInstanceValue = skipObservedWithContext( @@ -344,6 +349,10 @@ class ValueSpecificationObserver implements ValueSpecificationVisitor { visit_ColSpecArrayInstance(valueSpeciciation: ColSpecArrayInstance): void { observe_ColSpecArrayInstance(valueSpeciciation, this.observerContext); } + + visit_ColSpecInstance(valueSpeciciation: ColSpecInstanceValue): void { + observe_ColSpecInstance(valueSpeciciation, this.observerContext); + } } export const observe_ValueSpecification = skipObservedWithContext( @@ -513,6 +522,18 @@ function _observe_ColSpecArrayInstance( return metamodel; } +function _observe_ColSpecInstance( + metamodel: ColSpecInstanceValue, + context: ObserverContext, +): ColSpecInstanceValue { + observe_Abstract_InstanceValue(metamodel, context); + makeObservable(metamodel, { + hashCode: override, + }); + metamodel.values.forEach((value) => observe_ColSpec(value, context)); + return metamodel; +} + function _observe_ColSpec( metamodel: ColSpec, context: ObserverContext, diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_ValueSpecificationTransformer.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_ValueSpecificationTransformer.ts index 0f4a94b735..6150444f29 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_ValueSpecificationTransformer.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_ValueSpecificationTransformer.ts @@ -86,7 +86,10 @@ import { V1_ClassInstance } from '../../../model/valueSpecification/raw/V1_Class import { V1_ClassInstanceType } from '../../pureProtocol/serializationHelpers/V1_ValueSpecificationSerializer.js'; import type { KeyExpressionInstanceValue } from '../../../../../../../graph/metamodel/pure/valueSpecification/KeyExpressionInstanceValue.js'; import { V1_CByteArray } from '../../../model/valueSpecification/raw/V1_CByteArray.js'; -import type { ColSpecArrayInstance } from '../../../../../../../graph/metamodel/pure/valueSpecification/RelationValueSpecification.js'; +import type { + ColSpecArrayInstance, + ColSpecInstanceValue, +} from '../../../../../../../graph/metamodel/pure/valueSpecification/RelationValueSpecification.js'; import { V1_ColSpecArray } from '../../../model/valueSpecification/raw/classInstance/relation/V1_ColSpecArray.js'; import { V1_ColSpec } from '../../../model/valueSpecification/raw/classInstance/relation/V1_ColSpec.js'; import { RelationColumn } from '../../../../../../../graph/metamodel/pure/packageableElements/relation/RelationType.js'; @@ -424,6 +427,29 @@ class V1_ValueSpecificationTransformer classInstance.value = colSpecArray; return classInstance; } + + visit_ColSpecInstance( + valueSpecification: ColSpecInstanceValue, + ): V1_ValueSpecification { + const classInstance = new V1_ClassInstance(); + classInstance.type = V1_ClassInstanceType.COL_SPEC; + const val = guaranteeNonNullable(valueSpecification.values[0]); + const colProtocol = new V1_ColSpec(); + colProtocol.name = val.name; + const fun1 = val.function1?.accept_ValueSpecificationVisitor( + new V1_ValueSpecificationTransformer( + this.inScope, + this.open, + this.isParameter, + this.useAppliedFunction, + ), + ); + if (fun1) { + colProtocol.function1 = guaranteeType(fun1, V1_Lambda); + } + classInstance.value = colProtocol; + return classInstance; + } } export function V1_transformGraphFetchTree( diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_ValueSpecificationBuilderHelper.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_ValueSpecificationBuilderHelper.ts index 7ae250b79b..dce5dfb559 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_ValueSpecificationBuilderHelper.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_ValueSpecificationBuilderHelper.ts @@ -114,6 +114,11 @@ import { V1_getGenericTypeFullPath, V1_createGenericTypeWithElementPath, } from '../../../../helpers/V1_DomainHelper.js'; +import { + ColSpec, + ColSpecInstanceValue, +} from '../../../../../../../../graph/metamodel/pure/valueSpecification/RelationValueSpecification.js'; +import { V1_ColSpec } from '../../../../model/valueSpecification/raw/classInstance/relation/V1_ColSpec.js'; const buildPrimtiveInstanceValue = ( type: PRIMITIVE_TYPE, @@ -458,6 +463,17 @@ export class V1_ValueSpecificationBuilder instanceValue.values = [tree]; return instanceValue; } + case V1_ClassInstanceType.COL_SPEC: { + const instanceValue = new ColSpecInstanceValue( + Multiplicity.ONE, + undefined, + ); + const protocol = guaranteeType(valueSpecification.value, V1_ColSpec); + const value = new ColSpec(); + value.name = protocol.name; + instanceValue.values = [value]; + return instanceValue; + } default: { const builders = this.context.extensions.plugins.flatMap( (plugin) => plugin.V1_getExtraClassInstanceValueBuilders?.() ?? [], diff --git a/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/RelationValueSpecification.ts b/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/RelationValueSpecification.ts index 37d385f8f5..c01974a8ba 100644 --- a/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/RelationValueSpecification.ts +++ b/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/RelationValueSpecification.ts @@ -41,7 +41,23 @@ export class ColSpec implements Hashable { ]); } } +export class ColSpecInstanceValue extends InstanceValue implements Hashable { + override values: ColSpec[] = []; + override get hashCode(): string { + return hashArray([ + CORE_HASH_STRUCTURE.RELATION_COL_SPEC, + this.genericType?.ownerReference.valueForSerialization ?? '', + this.multiplicity, + hashArray(this.values), + ]); + } + override accept_ValueSpecificationVisitor( + visitor: ValueSpecificationVisitor, + ): T { + return visitor.visit_ColSpecInstance(this); + } +} export class ColSpecArray implements Hashable { colSpecs: ColSpec[] = []; diff --git a/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/ValueSpecification.ts b/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/ValueSpecification.ts index 8acc79a405..a9ecec56e4 100644 --- a/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/ValueSpecification.ts +++ b/packages/legend-graph/src/graph/metamodel/pure/valueSpecification/ValueSpecification.ts @@ -34,7 +34,10 @@ import type { VariableExpression } from './VariableExpression.js'; import type { INTERNAL__PropagatedValue } from './INTERNAL__PropagatedValue.js'; import type { Hashable } from '@finos/legend-shared'; import type { KeyExpressionInstanceValue } from './KeyExpressionInstanceValue.js'; -import type { ColSpecArrayInstance } from './RelationValueSpecification.js'; +import type { + ColSpecArrayInstance, + ColSpecInstanceValue, +} from './RelationValueSpecification.js'; export interface ValueSpecificationVisitor { visit_INTERNAL__UnknownValueSpecification( @@ -55,6 +58,7 @@ export interface ValueSpecificationVisitor { visit_InstanceValue(valueSpecification: InstanceValue): T; visit_ColSpecArrayInstance(valueSpeciciation: ColSpecArrayInstance): T; + visit_ColSpecInstance(valueSpeciciation: ColSpecInstanceValue): T; visit_CollectionInstanceValue(valueSpecification: CollectionInstanceValue): T; visit_EnumValueInstanceValue(valueSpecification: EnumValueInstanceValue): T; diff --git a/packages/legend-graph/src/index.ts b/packages/legend-graph/src/index.ts index 7fa4dcf53a..bd0189b219 100644 --- a/packages/legend-graph/src/index.ts +++ b/packages/legend-graph/src/index.ts @@ -106,6 +106,7 @@ export { } from './graph/metamodel/pure/valueSpecification/KeyExpressionInstanceValue.js'; export { ColSpec, + ColSpecInstanceValue, ColSpecArray, ColSpecArrayInstance, } from './graph/metamodel/pure/valueSpecification/RelationValueSpecification.js'; diff --git a/packages/legend-manual-tests/src/__tests__/query-builder/QueryRoundtripGrammar.engine-roundtrip-test.ts b/packages/legend-manual-tests/src/__tests__/query-builder/QueryRoundtripGrammar.engine-roundtrip-test.ts index 370890c3db..8176dc2c85 100644 --- a/packages/legend-manual-tests/src/__tests__/query-builder/QueryRoundtripGrammar.engine-roundtrip-test.ts +++ b/packages/legend-manual-tests/src/__tests__/query-builder/QueryRoundtripGrammar.engine-roundtrip-test.ts @@ -73,6 +73,37 @@ const TEST_CASES: QueryTestCase[] = [ model: 'Relational_Business', queryGrammar: "var_1: String[0..1]|model::pure::tests::model::simple::Person.all()->project([x|$x.firstName, x|$x.lastName], ['Edited First Name', 'Last Name'])", + convertedRelation: `var_1: String[0..1]|model::pure::tests::model::simple::Person.all()->project(~['Edited First Name':x|$x.firstName, 'Last Name':x|$x.lastName])`, + }, + { + testName: '[LEGACY] Result Modifier: Sort', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project([x|$x.companyName, x|$x.companyTitle], ['Company Name', 'Company Title'])->sort([asc('Company Name'), desc('Company Title')])`, + convertedRelation: `|showcase::northwind::model::crm::Customer.all()->project(~['Company Name':x|$x.companyName, 'Company Title':x|$x.companyTitle])->sort([~'Company Name'->ascending(), ~'Company Title'->descending()])`, + }, + { + testName: '[LEGACY] Result Modifier: Distinct', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project([x|$x.companyName], ['name'])->distinct()`, + convertedRelation: `|showcase::northwind::model::crm::Customer.all()->project(~[name:x|$x.companyName])->distinct()`, + }, + { + testName: '[LEGACY] Result Modifier: Take', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project([x|$x.companyName], ['Company Name'])->take(27)`, + convertedRelation: `|showcase::northwind::model::crm::Customer.all()->project(~['Company Name':x|$x.companyName])->limit(27)`, + }, + { + testName: '[LEGACY] Result Modifier: Slice', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project([x|$x.companyName], ['Company Name'])->slice(1, 7)`, + convertedRelation: `|showcase::northwind::model::crm::Customer.all()->project(~['Company Name':x|$x.companyName])->slice(1, 7)`, + }, + { + testName: '[LEGACY] Result Modifier: For Watermark', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->forWatermark('testing')->project([x|$x.companyName], ['Company Name'])`, + convertedRelation: `|showcase::northwind::model::crm::Customer.all()->forWatermark('testing')->project(~['Company Name':x|$x.companyName])`, }, // Relation { @@ -93,6 +124,26 @@ const TEST_CASES: QueryTestCase[] = [ queryGrammar: "|showcase::northwind::model::crm::Customer.all()->filter(x|$x.companyTitle == 'company title')->project(~['Company Name':x|$x.companyName, 'Company Title':x|$x.companyTitle])->filter(row|$row.'Company Name' == 'company name')", }, + { + testName: '[LEGACY] Result Modifier: Sort', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project([x|$x.companyName, x|$x.companyTitle], ['Company Name', 'Company Title'])->sort([asc('Company Name'), desc('Company Title')])`, + }, + { + testName: '[LEGACY] Result Modifier: Distinct', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project(~[name:x|$x.companyName])->distinct()`, + }, + { + testName: '[LEGACY] Result Modifier: Take', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project(~['Company Name':x|$x.companyName])->limit(27)`, + }, + { + testName: '[LEGACY] Result Modifier: Slice', + model: 'Northwind', + queryGrammar: `|showcase::northwind::model::crm::Customer.all()->project(~['Company Name':x|$x.companyName])->slice(1, 7)`, + }, // conversion { testName: '[CONVERSION] Convert Query With Pre/Post Filter to Relation', diff --git a/packages/legend-query-builder/src/graph/QueryBuilderMetaModelConst.ts b/packages/legend-query-builder/src/graph/QueryBuilderMetaModelConst.ts index 647aad33b0..79ffb745d1 100644 --- a/packages/legend-query-builder/src/graph/QueryBuilderMetaModelConst.ts +++ b/packages/legend-query-builder/src/graph/QueryBuilderMetaModelConst.ts @@ -121,7 +121,11 @@ export enum QUERY_BUILDER_SUPPORTED_FUNCTIONS { // Relation RELATION_PROJECT = 'meta::pure::functions::relation::project', RELATION_LIMIT = 'meta::pure::functions::relation::limit', - + RELATION_ASC = 'meta::pure::functions::relation::ascending', + RELATION_DESC = 'meta::pure::functions::relation::descending', + RELATION_DISTINCT = 'meta::pure::functions::relation::distinct', + RELATION_SORT = 'meta::pure::functions::relation::sort', + RELATION_SLICE = 'meta::pure::functions::relation::slice', // filter CONTAINS = 'meta::pure::functions::string::contains', ENDS_WITH = 'meta::pure::functions::string::endsWith', diff --git a/packages/legend-query-builder/src/stores/QueryBuilderStateBuilder.ts b/packages/legend-query-builder/src/stores/QueryBuilderStateBuilder.ts index 10f31ef2e5..934118c329 100644 --- a/packages/legend-query-builder/src/stores/QueryBuilderStateBuilder.ts +++ b/packages/legend-query-builder/src/stores/QueryBuilderStateBuilder.ts @@ -54,6 +54,7 @@ import { RuntimePointer, PackageableElementExplicitReference, MILESTONING_STEREOTYPE, + type ColSpecInstanceValue, } from '@finos/legend-graph'; import { processTDSPostFilterExpression } from './fetch-structure/tds/post-filter/QueryBuilderPostFilterStateBuilder.js'; import { processFilterExpression } from './filter/QueryBuilderFilterStateBuilder.js'; @@ -69,6 +70,7 @@ import { processInternalizeExpression, } from './fetch-structure/graph-fetch/QueryBuilderGraphFetchTreeStateBuilder.js'; import { + processRelationSortDirectionExpression, processTDSColExpression, processTDSDistinctExpression, processTDSProjectExpression, @@ -669,10 +671,10 @@ export class QueryBuilderValueSpecificationProcessor processTDSColExpression(valueSpecification, this.queryBuilderState); return; } else if ( - matchFunctionName( - functionName, + matchFunctionName(functionName, [ QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_TAKE, - ) + QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_LIMIT, + ]) ) { processTDSTakeExpression( valueSpecification, @@ -725,6 +727,18 @@ export class QueryBuilderValueSpecificationProcessor this.queryBuilderState, ); return; + } else if ( + matchFunctionName(functionName, [ + QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_ASC, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_DESC, + ]) + ) { + processRelationSortDirectionExpression( + valueSpecification, + this.parentExpression, + this.queryBuilderState, + ); + return; } else if ( matchFunctionName( functionName, @@ -994,6 +1008,10 @@ export class QueryBuilderValueSpecificationProcessor `Can't process col spec array expression with parent expression of function ${this.parentExpression.functionName}()`, ); } + + visit_ColSpecInstance(valueSpeciciation: ColSpecInstanceValue): void { + throw new Error('Method not implemented.'); + } } export const processParameters = ( diff --git a/packages/legend-query-builder/src/stores/__tests__/TEST_DATA__QueryBuilder_Generic.ts b/packages/legend-query-builder/src/stores/__tests__/TEST_DATA__QueryBuilder_Generic.ts index 5e3e9827b4..3df04a19d4 100644 --- a/packages/legend-query-builder/src/stores/__tests__/TEST_DATA__QueryBuilder_Generic.ts +++ b/packages/legend-query-builder/src/stores/__tests__/TEST_DATA__QueryBuilder_Generic.ts @@ -124,7 +124,7 @@ export const TEST_DATA_simpleTypedRelationProjection = { body: [ { _type: 'func', - function: 'take', + function: 'limit', parameters: [ { _type: 'func', diff --git a/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionStateBuilder.ts b/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionStateBuilder.ts index e8f653a821..3970a6b525 100644 --- a/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionStateBuilder.ts +++ b/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionStateBuilder.ts @@ -29,6 +29,7 @@ import { VariableExpression, PrimitiveInstanceValue, LambdaFunctionInstanceValue, + ColSpecInstanceValue, } from '@finos/legend-graph'; import { assertNonNullable, @@ -603,7 +604,7 @@ export const processTDSSortDirectionExpression = ( // check parameters assertTrue( expression.parametersValues.length === 1, - `Can't process ${functionName}() expression: ${functionName}() expects no argument`, + `Can't process ${functionName}() expression: ${functionName}() expects one argument`, ); // build state @@ -633,3 +634,65 @@ export const processTDSSortDirectionExpression = ( } } }; + +export const processRelationSortDirectionExpression = ( + expression: SimpleFunctionExpression, + parentExpression: SimpleFunctionExpression | undefined, + queryBuilderState: QueryBuilderState, +): void => { + const functionName = expression.functionName; + + // check parent expression + assertTrue( + Boolean( + parentExpression && + matchFunctionName( + parentExpression.functionName, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_SORT, + ), + ), + `Can't process ${functionName}() expression: only support ${functionName}() used within a sort() expression`, + ); + + // check parameters + assertTrue( + expression.parametersValues.length === 1, + `Can't process ${functionName}() expression: ${functionName}() expects one argument`, + ); + + // build state + if ( + queryBuilderState.fetchStructureState.implementation instanceof + QueryBuilderTDSState + ) { + const projectionState = + queryBuilderState.fetchStructureState.implementation; + const value = guaranteeType( + expression.parametersValues[0], + ColSpecInstanceValue, + ); + assertTrue( + value.values.length === 1, + `Can't process ${functionName}() expression: Col Spec Instance Value expects one value`, + ); + const sortColumnName = guaranteeNonNullable( + value.values[0], + `Col Spec value expected in Col Spec Instance Value`, + ).name; + const queryBuilderProjectionColumnState = projectionState.tdsColumns.find( + (e) => e.columnName === sortColumnName, + ); + if (queryBuilderProjectionColumnState) { + const sortColumnState = new SortColumnState( + queryBuilderProjectionColumnState, + ); + sortColumnState.sortType = matchFunctionName( + functionName, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_ASC, + ) + ? COLUMN_SORT_TYPE.ASC + : COLUMN_SORT_TYPE.DESC; + projectionState.resultSetModifierState.addSortColumn(sortColumnState); + } + } +}; diff --git a/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionValueSpecificationBuilder.ts b/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionValueSpecificationBuilder.ts index 8eff89b759..3e42e9b3dd 100644 --- a/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionValueSpecificationBuilder.ts +++ b/packages/legend-query-builder/src/stores/fetch-structure/tds/projection/QueryBuilderProjectionValueSpecificationBuilder.ts @@ -27,7 +27,6 @@ import { V1_serializeRawValueSpecification, V1_transformRawLambda, V1_GraphTransformerContextBuilder, - matchFunctionName, PrimitiveType, LambdaFunctionInstanceValue, } from '@finos/legend-graph'; @@ -42,10 +41,6 @@ import { QueryBuilderSimpleProjectionColumnState, } from './QueryBuilderProjectionColumnState.js'; import type { QueryBuilderTDSState } from '../QueryBuilderTDSState.js'; -import { - type QueryResultSetModifierState, - type SortColumnState, -} from '../QueryResultSetModifierState.js'; import { QUERY_BUILDER_SUPPORTED_FUNCTIONS } from '../../../../graph/QueryBuilderMetaModelConst.js'; import { buildGenericLambdaFunctionInstanceValue } from '../../../QueryBuilderValueSpecificationHelper.js'; import { @@ -54,139 +49,9 @@ import { } from '../../../QueryBuilderValueSpecificationBuilderHelper.js'; import { appendOLAPGroupByState } from '../window/QueryBuilderWindowValueSpecificationBuilder.js'; import { appendPostFilter } from '../post-filter/QueryBuilderPostFilterValueSpecificationBuilder.js'; -import { buildTDSSortTypeExpression } from '../QueryBuilderTDSHelper.js'; import { buildRelationProjection } from './QueryBuilderRelationProjectValueSpecBuidler.js'; import { QueryBuilderAggregateOperator_Wavg } from '../aggregation/operators/QueryBuilderAggregateOperator_Wavg.js'; - -const buildSortExpression = ( - sortColumnState: SortColumnState, -): SimpleFunctionExpression => - buildTDSSortTypeExpression( - sortColumnState.sortType, - sortColumnState.columnState.columnName, - ); - -const appendResultSetModifier = ( - resultModifierState: QueryResultSetModifierState, - lambdaFunction: LambdaFunction, - options?: - | { - overridingLimit?: number | undefined; - withDataOverflowCheck?: boolean | undefined; - } - | undefined, -): LambdaFunction => { - if (lambdaFunction.expressionSequence.length === 1) { - const func = lambdaFunction.expressionSequence[0]; - if (func instanceof SimpleFunctionExpression) { - if ( - matchFunctionName(func.functionName, [ - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_PROJECT, - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_GROUP_BY, - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_FILTER, - QUERY_BUILDER_SUPPORTED_FUNCTIONS.OLAP_GROUPBY, - ]) - ) { - let currentExpression = func; - - // build distinct() - if (resultModifierState.distinct) { - const distinctFunction = new SimpleFunctionExpression( - extractElementNameFromPath( - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_DISTINCT, - ), - ); - distinctFunction.parametersValues[0] = currentExpression; - currentExpression = distinctFunction; - } - - // build sort() - if (resultModifierState.sortColumns.length) { - const sortFunction = new SimpleFunctionExpression( - extractElementNameFromPath( - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_SORT, - ), - ); - const multiplicity = - resultModifierState.tdsState.queryBuilderState.graphManagerState.graph.getMultiplicity( - resultModifierState.sortColumns.length, - resultModifierState.sortColumns.length, - ); - const collection = new CollectionInstanceValue( - multiplicity, - undefined, - ); - collection.values = - resultModifierState.sortColumns.map(buildSortExpression); - sortFunction.parametersValues[0] = currentExpression; - sortFunction.parametersValues[1] = collection; - currentExpression = sortFunction; - } - - // build take() - if (resultModifierState.limit || options?.overridingLimit) { - const limit = new PrimitiveInstanceValue( - GenericTypeExplicitReference.create( - new GenericType(PrimitiveType.INTEGER), - ), - ); - limit.values = [ - Math.min( - resultModifierState.limit - ? options?.withDataOverflowCheck - ? resultModifierState.limit + 1 - : resultModifierState.limit - : Number.MAX_SAFE_INTEGER, - options?.overridingLimit - ? options.withDataOverflowCheck - ? options.overridingLimit + 1 - : options.overridingLimit - : Number.MAX_SAFE_INTEGER, - ), - ]; - const takeFunction = new SimpleFunctionExpression( - extractElementNameFromPath( - QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_TAKE, - ), - ); - takeFunction.parametersValues[0] = currentExpression; - takeFunction.parametersValues[1] = limit; - currentExpression = takeFunction; - } - // build slice() - if (resultModifierState.slice) { - const sliceStart = resultModifierState.slice[0]; - const sliceEnd = resultModifierState.slice[1]; - const startVal = new PrimitiveInstanceValue( - GenericTypeExplicitReference.create( - new GenericType(PrimitiveType.INTEGER), - ), - ); - const endVal = new PrimitiveInstanceValue( - GenericTypeExplicitReference.create( - new GenericType(PrimitiveType.INTEGER), - ), - ); - startVal.values = [sliceStart]; - endVal.values = [sliceEnd]; - const sliceFunction = new SimpleFunctionExpression( - extractElementNameFromPath(QUERY_BUILDER_SUPPORTED_FUNCTIONS.SLICE), - ); - sliceFunction.parametersValues = [ - currentExpression, - startVal, - endVal, - ]; - currentExpression = sliceFunction; - } - - lambdaFunction.expressionSequence[0] = currentExpression; - return lambdaFunction; - } - } - } - return lambdaFunction; -}; +import { appendResultSetModifier } from '../result-modifier/ResultModifierValueSpecificationBuilder.js'; const buildProjectColFunc = ( tdsState: QueryBuilderTDSState, @@ -542,14 +407,19 @@ export const appendProjection = ( appendPostFilter(tdsState.postFilterState, lambdaFunction); // build result set modifiers - appendResultSetModifier(tdsState.resultSetModifierState, lambdaFunction, { - overridingLimit: - options?.isBuildingExecutionQuery && !options.isExportingResult - ? queryBuilderState.resultState.previewLimit - : undefined, - withDataOverflowCheck: - options?.isBuildingExecutionQuery && !options.isExportingResult - ? options.withDataOverflowCheck - : undefined, - }); + appendResultSetModifier( + tdsState.resultSetModifierState, + lambdaFunction, + tdsState.queryBuilderState.isFetchStructureTyped, + { + overridingLimit: + options?.isBuildingExecutionQuery && !options.isExportingResult + ? queryBuilderState.resultState.previewLimit + : undefined, + withDataOverflowCheck: + options?.isBuildingExecutionQuery && !options.isExportingResult + ? options.withDataOverflowCheck + : undefined, + }, + ); }; diff --git a/packages/legend-query-builder/src/stores/fetch-structure/tds/result-modifier/ResultModifierValueSpecificationBuilder.ts b/packages/legend-query-builder/src/stores/fetch-structure/tds/result-modifier/ResultModifierValueSpecificationBuilder.ts new file mode 100644 index 0000000000..f02c6ddcb6 --- /dev/null +++ b/packages/legend-query-builder/src/stores/fetch-structure/tds/result-modifier/ResultModifierValueSpecificationBuilder.ts @@ -0,0 +1,337 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + type GraphManagerState, + type LambdaFunction, + type ValueSpecification, + ColSpec, + ColSpecInstanceValue, + CollectionInstanceValue, + GenericType, + GenericTypeExplicitReference, + Multiplicity, + PrimitiveInstanceValue, + PrimitiveType, + SimpleFunctionExpression, + createPrimitiveInstance_String, + extractElementNameFromPath, + matchFunctionName, +} from '@finos/legend-graph'; +import { + COLUMN_SORT_TYPE, + QUERY_BUILDER_SUPPORTED_FUNCTIONS, +} from '../../../../graph/QueryBuilderMetaModelConst.js'; +import type { + QueryResultSetModifierState, + SortColumnState, +} from '../QueryResultSetModifierState.js'; +import { guaranteeNonNullable } from '@finos/legend-shared'; + +export type ResultModifierValueSpecOptions = { + overridingLimit?: number | undefined; + withDataOverflowCheck?: boolean | undefined; +}; + +export abstract class ResultModifierValueSpecificationBuilder { + _currentResultModifierFunc: SimpleFunctionExpression | undefined; + readonly graphManagerState: GraphManagerState; + options: ResultModifierValueSpecOptions | undefined; + + distinct = false; + sortColumns: SortColumnState[] | undefined; + limit?: number | undefined; + slice: [number, number] | undefined; + + constructor(graphManagerState: GraphManagerState) { + this.graphManagerState = graphManagerState; + } + + get currentExpression(): SimpleFunctionExpression { + return guaranteeNonNullable( + this._currentResultModifierFunc, + `Current expression needs to be defined to build result modifier`, + ); + } + // function + abstract get distinctFunctionName(): string; + abstract get sortFunctionName(): string; + abstract get limitFunctionName(): string; + abstract get sliceFunctionName(): string; + abstract get ascFunctionname(): string; + abstract get descFunctionName(): string; + + get supportedResultModifiersFunctions(): string[] { + return [ + QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_PROJECT, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_GROUP_BY, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_FILTER, + QUERY_BUILDER_SUPPORTED_FUNCTIONS.OLAP_GROUPBY, + ]; + } + getSortTypeFunctionName(sortType: COLUMN_SORT_TYPE): string { + return sortType === COLUMN_SORT_TYPE.ASC + ? this.ascFunctionname + : this.descFunctionName; + } + + // builders + setCurrentResultModifierFunction(val: SimpleFunctionExpression): void { + this._currentResultModifierFunc = val; + } + + withOptions( + options: ResultModifierValueSpecOptions | undefined, + ): ResultModifierValueSpecificationBuilder { + this.options = options; + return this; + } + + withDistinct(distinct: boolean): ResultModifierValueSpecificationBuilder { + this.distinct = distinct; + return this; + } + + withSortColumns( + sortCols: SortColumnState[] | undefined, + ): ResultModifierValueSpecificationBuilder { + if (sortCols?.length) { + this.sortColumns = sortCols; + } + return this; + } + + withLimit( + limit: number | undefined, + ): ResultModifierValueSpecificationBuilder { + this.limit = limit; + return this; + } + + withSlice( + slice: [number, number] | undefined, + ): ResultModifierValueSpecificationBuilder { + this.slice = slice; + return this; + } + + // builder + buildDistinctFunction(): void { + const distinctFunction = new SimpleFunctionExpression( + extractElementNameFromPath(this.distinctFunctionName), + ); + distinctFunction.parametersValues[0] = this.currentExpression; + this.setCurrentResultModifierFunction(distinctFunction); + } + + buildSortFunction(sortColumns: SortColumnState[]): void { + const sortFunction = new SimpleFunctionExpression( + extractElementNameFromPath(this.sortFunctionName), + ); + const multiplicity = this.graphManagerState.graph.getMultiplicity( + sortColumns.length, + sortColumns.length, + ); + const collection = new CollectionInstanceValue(multiplicity, undefined); + collection.values = sortColumns.map((e) => this.buildSortExpression(e)); + sortFunction.parametersValues[0] = this.currentExpression; + sortFunction.parametersValues[1] = collection; + this.setCurrentResultModifierFunction(sortFunction); + } + + buildSortExpression( + sortColumnState: SortColumnState, + ): SimpleFunctionExpression { + const sortColumnFunction = new SimpleFunctionExpression( + extractElementNameFromPath( + this.getSortTypeFunctionName(sortColumnState.sortType), + ), + ); + sortColumnFunction.parametersValues[0] = this.buildColumnValueSpec( + sortColumnState.columnState.columnName, + ); + return sortColumnFunction; + } + + abstract buildColumnValueSpec(colName: string): ValueSpecification; + + buildLimitFunction(limit?: number | undefined) { + const limitPrimitiveInstanceVal = new PrimitiveInstanceValue( + GenericTypeExplicitReference.create( + new GenericType(PrimitiveType.INTEGER), + ), + ); + limitPrimitiveInstanceVal.values = [ + Math.min( + limit + ? this.options?.withDataOverflowCheck + ? limit + 1 + : limit + : Number.MAX_SAFE_INTEGER, + this.options?.overridingLimit + ? this.options.withDataOverflowCheck + ? this.options.overridingLimit + 1 + : this.options.overridingLimit + : Number.MAX_SAFE_INTEGER, + ), + ]; + const takeFunction = new SimpleFunctionExpression( + extractElementNameFromPath(this.limitFunctionName), + ); + takeFunction.parametersValues[0] = this.currentExpression; + takeFunction.parametersValues[1] = limitPrimitiveInstanceVal; + this.setCurrentResultModifierFunction(takeFunction); + } + + buildSliceFunction(slice: [number, number]): void { + const sliceStart = slice[0]; + const sliceEnd = slice[1]; + const startVal = new PrimitiveInstanceValue( + GenericTypeExplicitReference.create( + new GenericType(PrimitiveType.INTEGER), + ), + ); + const endVal = new PrimitiveInstanceValue( + GenericTypeExplicitReference.create( + new GenericType(PrimitiveType.INTEGER), + ), + ); + startVal.values = [sliceStart]; + endVal.values = [sliceEnd]; + const sliceFunction = new SimpleFunctionExpression( + extractElementNameFromPath(this.sliceFunctionName), + ); + sliceFunction.parametersValues = [this.currentExpression, startVal, endVal]; + this.setCurrentResultModifierFunction(sliceFunction); + } + + build(lambdaFunction: LambdaFunction): LambdaFunction { + if (lambdaFunction.expressionSequence.length === 1) { + const func = lambdaFunction.expressionSequence[0]; + if (func instanceof SimpleFunctionExpression) { + if ( + matchFunctionName( + func.functionName, + this.supportedResultModifiersFunctions, + ) + ) { + this._currentResultModifierFunc = func; + + // build distinct() + if (this.distinct) { + this.buildDistinctFunction(); + } + + // build sort() + if (this.sortColumns) { + this.buildSortFunction(this.sortColumns); + } + + // build take() + if (this.limit || this.options?.overridingLimit) { + this.buildLimitFunction(this.limit); + } + // build slice() + if (this.slice) { + this.buildSliceFunction(this.slice); + } + + lambdaFunction.expressionSequence[0] = this.currentExpression; + return lambdaFunction; + } + } + } + return lambdaFunction; + } +} + +export class TDSResultModifierValueSpecificationBuilder extends ResultModifierValueSpecificationBuilder { + override get limitFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_TAKE; + } + override get sliceFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.SLICE; + } + override get sortFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_SORT; + } + + override get distinctFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_DISTINCT; + } + + override get ascFunctionname(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_ASC; + } + override get descFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.TDS_DESC; + } + + override buildColumnValueSpec(columnName: string): ValueSpecification { + return createPrimitiveInstance_String(columnName); + } +} + +export class TypedResultModifierValueSpecificationBuilder extends ResultModifierValueSpecificationBuilder { + override get limitFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_LIMIT; + } + override get sliceFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_SLICE; + } + override get ascFunctionname(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_ASC; + } + override get descFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_DESC; + } + override get sortFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_SORT; + } + override get distinctFunctionName(): string { + return QUERY_BUILDER_SUPPORTED_FUNCTIONS.RELATION_DISTINCT; + } + + override buildColumnValueSpec(columnName: string): ValueSpecification { + const colSpec = new ColSpecInstanceValue(Multiplicity.ONE, undefined); + const value = new ColSpec(); + value.name = columnName; + colSpec.values = [value]; + return colSpec; + } +} + +export const appendResultSetModifier = ( + resultModifierState: QueryResultSetModifierState, + lambdaFunction: LambdaFunction, + isTyped: boolean, + options?: ResultModifierValueSpecOptions | undefined, +): LambdaFunction => { + const builder = isTyped + ? new TypedResultModifierValueSpecificationBuilder( + resultModifierState.tdsState.queryBuilderState.graphManagerState, + ) + : new TDSResultModifierValueSpecificationBuilder( + resultModifierState.tdsState.queryBuilderState.graphManagerState, + ); + return builder + .withOptions(options) + .withDistinct(resultModifierState.distinct) + .withSortColumns(resultModifierState.sortColumns) + .withLimit(resultModifierState.limit) + .withSlice(resultModifierState.slice) + .build(lambdaFunction); +};