diff --git a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/pom.xml b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/pom.xml
index 1d791173f14..f602def85a0 100644
--- a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/pom.xml
+++ b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/pom.xml
@@ -294,6 +294,10 @@
org.finos.legend.engine
legend-engine-pure-runtime-java-extension-compiled-functions-json
+
+ org.finos.legend.engine
+ legend-engine-pure-runtime-java-extension-compiled-functions-relation
+
org.finos.legend.pure
@@ -330,6 +334,11 @@
legend-engine-pure-runtime-java-extension-shared-functions-json
+
+ org.finos.legend.engine
+ legend-engine-pure-runtime-java-extension-shared-functions-relation
+
+
org.finos.legend.pure
legend-pure-runtime-java-extension-compiled-dsl-diagram
diff --git a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/extensions/extension.pure b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/extensions/extension.pure
index a57eaa65f34..00663a24865 100644
--- a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/extensions/extension.pure
+++ b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/extensions/extension.pure
@@ -73,6 +73,8 @@ Class meta::pure::extension::Extension
tdsSchema_resolveSchemaImpl : Function<{FunctionExpression[1], Map>[1], Extension[*] -> Pair, Function<{->SchemaState[1]}>>[*]}>[0..1];
//---------------------------------------------------------------------------------------------------------------------------------------
+ tdsToRelation: meta::pure::tds::toRelation::TdsToRelationExtension[0..1];
+
// testedBy navigates the Database structure. Should probably delete --------------------------------------------------------------------
testExtension_testedBy : Function<{ReferenceUsage[1], Extension[*] -> Function<{TestedByResult[1]->TestedByResult[1]}>[*]}>[0..1];
//---------------------------------------------------------------------------------------------------------------------------------------
diff --git a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/protocol/vX_X_X/transfers/helpers.pure b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/protocol/vX_X_X/transfers/helpers.pure
new file mode 100644
index 00000000000..9af20f282cb
--- /dev/null
+++ b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/protocol/vX_X_X/transfers/helpers.pure
@@ -0,0 +1,179 @@
+// Copyright 2025 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 meta::pure::extension::*;
+import meta::protocols::pure::vX_X_X::transformation::helpers::*;
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::var(name:String[1]): meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::Variable[1]
+{
+ var($name, [], [])
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::var(name:String[1], multiplicity:Multiplicity[0..1], genericType:meta::protocols::pure::vX_X_X::metamodel::m3::type::generics::GenericType[0..1]): meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::Variable[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::Variable(
+ _type = 'var',
+ name = $name,
+ genericType = $genericType,
+ multiplicity = if ($multiplicity->isNotEmpty(), | $multiplicity->toOne()->multiplicity(), | [])
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::multiplicity(value:Any[*]):meta::protocols::pure::vX_X_X::metamodel::m3::multiplicity::Multiplicity[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::m3::multiplicity::Multiplicity(lowerBound = $value->size(), upperBound = $value->size())
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::lambda(expressionSequence:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda[1]
+{
+ lambda($expressionSequence, [])
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::lambda(expressionSequence:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*], parameters: meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::Variable[*]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda(
+ _type = 'lambda',
+ parameters = $parameters,
+ body = $expressionSequence
+ )
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::collection(values:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::Collection[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::Collection(_type = 'collection',
+ values = $values,
+ multiplicity = ^meta::protocols::pure::vX_X_X::metamodel::m3::multiplicity::Multiplicity(lowerBound=$values->size(), upperBound=$values->size())
+ )
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecArrayInstance(names:String[*]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance[1]
+{
+ createColSpecArrayInstance(createColSpecArray($names))
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecArrayInstance(colSpecs:meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[*]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance[1]
+{
+ createColSpecArrayInstance($colSpecs->createColSpecArray())
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecArrayInstance(colSpecs:meta::protocols::pure::vX_X_X::metamodel::relation::ColSpecArray[1]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance(
+ _type = 'classInstance',
+ type = 'colSpecArray',
+ value = $colSpecs
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecInstance(colSpec:meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[1]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance(
+ _type = 'classInstance',
+ type = 'colSpec',
+ value = $colSpec
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpec(name:String[1]):meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[1]
+{
+ createColSpec($name, [], []);
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpec(name:String[1], func1:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda[0..1]):meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[1]
+{
+ createColSpec($name, $func1, []);
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpec(name:String[1], func1:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda[0..1], func2: meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Lambda[0..1]):meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec(
+ name = $name,
+ function1 = $func1,
+ function2 = $func2
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecArray(colspecs: meta::protocols::pure::vX_X_X::metamodel::relation::ColSpec[*]):meta::protocols::pure::vX_X_X::metamodel::relation::ColSpecArray[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::relation::ColSpecArray(
+ colSpecs = $colspecs
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecInstance(name:String[1]):meta::protocols::pure::vX_X_X::metamodel::valueSpecification::ClassInstance[1]
+{
+ createColSpecInstance(createColSpec($name, [], []))
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::createColSpecArray(names:String[*]):meta::protocols::pure::vX_X_X::metamodel::relation::ColSpecArray[1]
+{
+ createColSpecArray($names->map(n | $n->createColSpec([], [])))
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::appliedFunction(f:Function[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedFunction[1]
+{
+ appliedFunction($f, []);
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::appliedFunction(f:Function[1], parameters:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedFunction[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedFunction(
+ _type = 'func',
+ function = $f.functionName->toOne(),
+ fControl = $f.name,
+ parameters = $parameters
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::property(name:String[1], parameters:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedProperty[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedProperty(
+ _type = 'property',
+ property = $name,
+ parameters = $parameters
+ )
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::processExtractEnumValue(enum:Enum[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ processExtractEnumValue($enum->type()->cast(@Enumeration), $enum->toString());
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::processExtractEnumValue(enumeration:Enumeration[1], value:String[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ processExtractEnumValue($enumeration, ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::CString(_type = 'string', value = $value))
+}
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::processExtractEnumValue(enumeration:Enumeration[1], value:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ ^meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedProperty(
+ _type = 'property',
+ property = $value->match([
+ c:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::CString[1] | $c.value
+ ]),
+ parameters = transformAny($enumeration, [])
+ )
+}
+
+
+function meta::protocols::pure::vX_X_X::transformation::helpers::transformAny(value:Any[1], extensions:Extension[*]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ meta::protocols::pure::vX_X_X::transformation::fromPureGraph::valueSpecification::transformAny($value, [], ^Map>(), PureOne, $extensions);
+}
diff --git a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/tdsToRelation.pure b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/tdsToRelation.pure
new file mode 100644
index 00000000000..a3e86a27fca
--- /dev/null
+++ b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/tdsToRelation.pure
@@ -0,0 +1,481 @@
+// Copyright 2025 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 meta::protocols::pure::vX_X_X::transformation::helpers::*;
+import meta::protocols::pure::vX_X_X::metamodel::relation::*;
+import meta::pure::functions::math::olap::*;
+import meta::protocols::pure::vX_X_X::metamodel::valueSpecification::*;
+import meta::pure::extension::*;
+import meta::pure::tds::toRelation::*;
+import meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::*;
+import meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::*;
+
+Class meta::pure::tds::toRelation::TdsToRelationExtension {}
+
+Class meta::pure::tds::toRelation::TdsToRelationExtension_V_X_X extends TdsToRelationExtension {
+
+ transfers: Function<{AppliedFunction[1], Extension[*] -> meta::pure::functions::collection::Pair, FunctionDefinition<{->AppliedFunction[1]}>>[*]}>[1];
+}
+
+function meta::pure::tds::toRelation::transform(l:LambdaFunction[1], extensions:Extension[*]):Lambda[1]
+{
+ let transformed = $l->meta::protocols::pure::vX_X_X::transformation::fromPureGraph::transformLambda($extensions);
+ ^$transformed(body = $transformed.body->map(b | $b->transform($extensions)));
+}
+
+function meta::pure::tds::toRelation::transform(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1], extensions:Extension[*]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ $v->match([
+ a:AppliedFunction[1] | $a->transform($extensions),
+ a:AppliedProperty[1] |
+ if ($a.property->in(TDSRow.qualifiedProperties.name) && $a.parameters->size() == 2 && $a.parameters->at(1)->instanceOf(CString),
+ | property($a.parameters->at(1)->cast(@CString).value, $a.parameters->at(0)),
+ | $a),
+ l:Lambda[1] | ^$l(parameters = $l.parameters->map(p | $p->transform($extensions)->cast(@Variable)), body = $l.body->map(b | $b->transform($extensions))),
+ c:Collection[1] | ^$c(values = $c.values->map(v | $v->transform($extensions))),
+ v:Variable[1] | var($v.name),
+ v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1] | $v
+ ])
+}
+
+function meta::pure::tds::toRelation::transform(a:AppliedFunction[1], extensions:Extension[*]):AppliedFunction[1]
+{
+ [
+ pair(project_T_MANY__ColumnSpecification_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let columns = $a.parameters->at(1)->colsToColSpecArrayInstance($extensions);
+
+ appliedFunction(project_C_MANY__FuncColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $columns]);
+ ),
+ pair(project_K_MANY__Function_MANY__String_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let lambdas = $a.parameters->at(1)->fromCollection()->cast(@Lambda);
+ let names = $a.parameters->at(2)->fromCollection()->cast(@CString).value;
+
+ let columns = $lambdas->zip($names)
+ ->map(p |
+ createColSpec($p.second, $p.first, [])
+ )->createColSpecArrayInstance();
+
+ appliedFunction(project_C_MANY__FuncColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $columns]);
+ ),
+ //TODO activate when relation inference is fixed on project
+ // pair(project_TabularDataSet_1__ColumnSpecification_MANY__TabularDataSet_1_->cast(@Function),
+ // |
+ // let columns = $a.parameters->at(1)->colsToColSpecArrayInstance($extensions);
+
+ // appliedFunction(project_Relation_1__FuncColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $columns]);
+ // ),
+ pair(groupBy_K_MANY__Function_MANY__AggregateValue_MANY__String_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let groupByColumns = $a.parameters->at(1)->fromCollection()->cast(@Lambda);
+ let aggs = $a.parameters->at(2)->fromCollection()->cast(@ClassInstance).value->cast(@meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::AggregateValue);
+ let names = $a.parameters->at(3)->fromCollection()->cast(@CString).value;
+
+ let projects = $groupByColumns->concatenate($aggs.mapFn)->zip($names)
+ ->map(p | createColSpec($p.second, $p.first, []))
+ ->createColSpecArrayInstance();
+
+ let groupBys = $names->slice(0, $groupByColumns->size())
+ ->map(n | createColSpec($n, [], []))
+ ->createColSpecArrayInstance();
+
+ let aggregations = $names->slice($groupByColumns->size(), $names->size())->zip($aggs)
+ ->map(p |
+ let agg = $p.second.aggregateFn->transform($extensions)->cast(@Lambda);
+ createColSpec($p.first, lambda(property($p.first, $p.second.mapFn.parameters), $p.second.mapFn.parameters), $agg);
+ )->createColSpecArrayInstance();
+
+
+ appliedFunction(groupBy_Relation_1__ColSpecArray_1__AggColSpecArray_1__Relation_1_, [
+ appliedFunction(project_C_MANY__FuncColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $projects]),
+ $groupBys,
+ $aggregations
+ ]);
+ ),
+ pair(groupBy_TabularDataSet_1__String_MANY__AggregateValue_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let groupBys = $a.parameters->at(1)->fromCollection()->cast(@CString).value->createColSpecArrayInstance();
+ let aggs = $a.parameters->at(2)->fromCollection()->map(a |
+ $a->match([
+ c:ClassInstance[1] |
+ let agg = $c.value->cast(@meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TDSAggregateValue);
+ createColSpec($agg.name, $agg.mapFn->transform($extensions)->cast(@Lambda), $agg.aggregateFn->transform($extensions)->cast(@Lambda));,
+ a:AppliedFunction[1] |
+ let name = $a.parameters->at(0)->cast(@CString).value;
+ let map = $a.parameters->at(1)->transform($extensions)->cast(@Lambda);
+ let agg = $a.parameters->at(2)->transform($extensions)->cast(@Lambda);
+ createColSpec($name, $map, $agg);
+ ])
+ )->createColSpecArrayInstance();
+
+ appliedFunction(groupBy_Relation_1__ColSpecArray_1__AggColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $groupBys, $aggs]);
+ ),
+ pair(sort_TabularDataSet_1__SortInformation_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let items = $a.parameters->at(1)->fromCollection()->map(p | $p->match([
+ f:AppliedFunction[1] |
+ let column = $f.parameters->at(0)->cast(@CString).value->createColSpecInstance();
+ [
+ pair(asc_String_1__SortInformation_1_.name->toOne(),
+ | appliedFunction(ascending_ColSpec_1__SortInfo_1_, $column)),
+ pair(desc_String_1__SortInformation_1_.name->toOne(),
+ | appliedFunction(descending_ColSpec_1__SortInfo_1_, $column))
+ ]->getValue($f.fControl->toOne())->eval();,
+ s:ClassInstance[1] |
+ let si = $s.value->cast(@TDSSortInformation);
+ let column = createColSpecInstance($si.column);
+ [
+ pair(SortDirection.ASC.name,
+ | appliedFunction(ascending_ColSpec_1__SortInfo_1_, $column)),
+ pair(SortDirection.DESC.name,
+ | appliedFunction(descending_ColSpec_1__SortInfo_1_, $column))
+ ]->getValue($si.direction)->eval();
+ ]))->toCollection(false);
+
+ appliedFunction(sort_Relation_1__SortInfo_MANY__Relation_1_, [$a.parameters->at(0)->transform($extensions), $items]);
+ ),
+ pair(filter_TabularDataSet_1__Function_1__TabularDataSet_1_->cast(@Function),
+ |
+ let func = $a.parameters->at(1)->transform($extensions)->cast(@Lambda);
+
+ appliedFunction(filter_Relation_1__Function_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $func]);
+ ),
+ pair(extend_TabularDataSet_1__BasicColumnSpecification_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let columns = $a.parameters->at(1)->colsToColSpecArrayInstance($extensions);
+ appliedFunction(extend_Relation_1__FuncColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $columns]);
+ ),
+ pair(renameColumns_TabularDataSet_1__Pair_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let renames = $a.parameters->at(1)->fromCollection()->map(p |
+ $p->match([
+ c:ClassInstance[1] |
+ let pair = $c.value->cast(@meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::Pair);
+ pair($pair.first->cast(@CString).value, $pair.second->cast(@CString).value);,
+ a:AppliedFunction[1] |
+ pair($a.parameters->at(0)->cast(@CString).value, $a.parameters->at(1)->cast(@CString).value)
+ ])
+ );
+
+ $renames->fold({p, acc |
+ appliedFunction(rename_Relation_1__ColSpec_1__ColSpec_1__Relation_1_, [$acc, createColSpecInstance($p.first), createColSpecInstance($p.second)])
+ }, $a.parameters->at(0)->transform($extensions)->cast(@AppliedFunction));
+ ),
+ pair(renameColumn_TabularDataSet_1__String_1__String_1__TabularDataSet_1_->cast(@Function),
+ |
+ let from = $a.parameters->at(1)->cast(@CString).value->createColSpecInstance();
+ let to = $a.parameters->at(2)->cast(@CString).value->createColSpecInstance();
+
+ appliedFunction(rename_Relation_1__ColSpec_1__ColSpec_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $from, $to]);
+ ),
+ pair(restrict_TabularDataSet_1__String_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let columns = $a.parameters->at(1)->fromCollection()->cast(@CString).value->createColSpecArrayInstance();
+
+ appliedFunction(select_Relation_1__ColSpecArray_1__Relation_1_, [$a.parameters->at(0)->transform($extensions), $columns]);
+ ),
+ pair(restrictDistinct_TabularDataSet_1__String_MANY__TabularDataSet_1_->cast(@Function),
+ |
+ let columns = $a.parameters->at(1)->fromCollection()->cast(@CString).value->createColSpecArrayInstance();
+
+ let args = [$a.parameters->at(0)->transform($extensions), $columns];
+ appliedFunction(distinct_Relation_1__Relation_1_, appliedFunction(select_Relation_1__ColSpecArray_1__Relation_1_, [$args]));
+ ),
+ pair(olapGroupBy_TabularDataSet_1__String_MANY__SortInformation_$0_1$__OlapOperation_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByStringSortOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__String_MANY__SortInformation_$0_1$__FunctionDefinition_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByStringSortOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__String_MANY__OlapOperation_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByStringOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__OlapOperation_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__SortInformation_$0_1$__OlapOperation_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupBySortOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__String_MANY__FunctionDefinition_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByStringOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__FunctionDefinition_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupByOpString($a, $extensions)
+ ),
+ pair(olapGroupBy_TabularDataSet_1__SortInformation_$0_1$__FunctionDefinition_1__String_1__TabularDataSet_1_->cast(@Function),
+ | transformOlapGroupBySortOpString($a, $extensions)
+ )
+ ]->concatenate([
+ swap(drop_TabularDataSet_1__Integer_1__TabularDataSet_1_, drop_Relation_1__Integer_1__Relation_1_, $a, $extensions),
+ swap(slice_TabularDataSet_1__Integer_1__Integer_1__TabularDataSet_1_, slice_Relation_1__Integer_1__Integer_1__Relation_1_, $a, $extensions),
+ swap(limit_TabularDataSet_1__Integer_1__TabularDataSet_1_, limit_Relation_1__Integer_1__Relation_1_, $a, $extensions),
+ swap(take_TabularDataSet_1__Integer_1__TabularDataSet_1_, limit_Relation_1__Integer_1__Relation_1_, $a, $extensions),
+ swap(distinct_TabularDataSet_1__TabularDataSet_1_, distinct_Relation_1__Relation_1_, $a, $extensions),
+ swap(concatenate_TabularDataSet_1__TabularDataSet_1__TabularDataSet_1_, concatenate_Relation_1__Relation_1__Relation_1_, $a, $extensions)
+ ])->concatenate(getExtensions($a, $extensions))->getValue(resolveFControlFunction($a, $extensions), | ^$a(parameters = $a.parameters->map(p | $p->transform($extensions))))->eval();
+}
+
+function meta::pure::tds::toRelation::getExtensions(a:AppliedFunction[1], extensions:Extension[*]):meta::pure::functions::collection::Pair, FunctionDefinition<{->AppliedFunction[1]}>>[*]
+{
+ $extensions.tdsToRelation
+ ->filter(t | $t->instanceOf(TdsToRelationExtension_V_X_X))
+ ->cast(@TdsToRelationExtension_V_X_X).transfers->map(t | $t->eval($a, $extensions))
+}
+
+
+
+function meta::pure::tds::toRelation::swap(f:Function[1], o:Function[1], a:AppliedFunction[1], extensions:Extension[*]):meta::pure::functions::collection::Pair, FunctionDefinition<{->AppliedFunction[1]}>>[1]
+{
+ pair($f, | appliedFunction($o, $a.parameters->map(p | $p->transform($extensions))))
+}
+
+function meta::pure::tds::toRelation::colsToColSpecArrayInstance(params:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*], extensions:Extension[*]):ClassInstance[1]
+{
+ $params->fromCollection()->map(i | $i->match([
+ c:ClassInstance[1] |
+ let col = $c.value->cast(@TDSColumnInformation);
+ createColSpec($col.name, $col.columnFn, []);,
+ f:AppliedFunction[1] |
+ createColSpec($f.parameters->at(1)->cast(@CString).value, $f.parameters->at(0)->transform($extensions)->cast(@Lambda), [])
+ ]))->createColSpecArrayInstance();
+}
+
+function meta::pure::tds::toRelation::fromCollection(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]
+{
+ $v->match([
+ c:Collection[1] | $c.values,
+ v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*] | $v
+ ])
+}
+
+function meta::pure::tds::toRelation::toCollection(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]):Collection[1]
+{
+ toCollection($v, true)->cast(@Collection);
+}
+
+function meta::pure::tds::toRelation::toCollection(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*], force:Boolean[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ $v->match([
+ c:Collection[1] | $c,
+ c:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*]
+ | if($c->size() == 1 && !$force, | $c->toOne(), | collection($c))
+ ])
+}
+
+function meta::pure::tds::toRelation::transformOlapGroupBySortOpString(a:AppliedFunction[1], extensions:Extension[*]):AppliedFunction[1]
+{
+ let name = $a.parameters->at(3)->cast(@CString).value;
+ let agg = $a.parameters->at(2)->transformOlapFunction($name, $extensions)->createColSpecArrayInstance();
+ let sort = $a.parameters->at(1)->transformSortInfo()->toCollection(false);
+ let func = $a.parameters->at(2)->getOlapExtendFunction();
+
+ let over = appliedFunction(over_SortInfo_MANY___Window_1_, [$sort]);
+
+ appliedFunction($func, [$a.parameters->at(0)->transform($extensions), $over, $agg]);
+}
+
+function meta::pure::tds::toRelation::transformOlapGroupByOpString(a:AppliedFunction[1], extensions:Extension[*]):AppliedFunction[1]
+{
+ let name = $a.parameters->at(2)->cast(@CString).value;
+ let agg = $a.parameters->at(1)->transformOlapFunction($name, $extensions)->createColSpecArrayInstance();
+ let func = $a.parameters->at(1)->getOlapExtendFunction();
+
+ let over = appliedFunction(over_String_MANY__SortInfo_MANY__Frame_$0_1$___Window_1_, [collection([]), collection([]), collection([])]);
+
+ appliedFunction($func, [$a.parameters->at(0)->transform($extensions), $over, $agg]);
+}
+
+function meta::pure::tds::toRelation::transformOlapGroupByStringOpString(a:AppliedFunction[1], extensions:Extension[*]):AppliedFunction[1]
+{
+ let columns = $a.parameters->at(1)->fromCollection()->cast(@CString).value->createColSpecArrayInstance();
+ let name = $a.parameters->at(3)->cast(@CString).value;
+ let agg = $a.parameters->at(2)->transformOlapFunction($name, $extensions)->createColSpecArrayInstance();
+ let func = $a.parameters->at(2)->getOlapExtendFunction();
+
+ let over = appliedFunction(over_ColSpecArray_1___Window_1_, [$columns]);
+
+ appliedFunction($func, [$a.parameters->at(0)->transform($extensions), $over, $agg]);
+}
+function meta::pure::tds::toRelation::transformOlapGroupByStringSortOpString(a:AppliedFunction[1], extensions:Extension[*]):AppliedFunction[1]
+{
+ let columns = $a.parameters->at(1)->fromCollection()->cast(@CString).value->createColSpecArrayInstance();
+ let sort = $a.parameters->at(2)->transformSortInfo()->toCollection(false);
+ let name = $a.parameters->at(4)->cast(@CString).value;
+ let agg = $a.parameters->at(3)->transformOlapFunction($name, $extensions)->createColSpecArrayInstance();
+ let func = $a.parameters->at(3)->getOlapExtendFunction();
+
+ let over = appliedFunction(over_ColSpecArray_1__SortInfo_MANY___Window_1_, [$columns, $sort]);
+
+ appliedFunction($func, [$a.parameters->at(0)->transform($extensions), $over, $agg]);
+}
+
+function meta::pure::tds::toRelation::transformOlapFunction(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1], name:String[1], extensions:Extension[*]):ColSpec[1]
+{
+ $v->match([
+ c:ClassInstance[1] |
+ $c.value->match([
+ r:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapRank[1] |
+ let var = $r.function.parameters->at(0).name;
+
+ assert($r.function.body->size() == 1 && $r.function.body->toOne()->instanceOf(AppliedFunction), 'olap func must be a simple lambda');
+
+ let func = [
+ pair(rowNumber_Any_MANY__Map_1_.name->toOne(), | appliedFunction(rowNumber_Relation_1__T_1__Integer_1_, [var('p'), var('r')])),
+ pair(denseRank_Any_MANY__Map_1_.name->toOne(), | appliedFunction(denseRank_Relation_1___Window_1__T_1__Integer_1_, [var('p'), var('w'), var('r')])),
+ pair(rank_Any_MANY__Map_1_.name->toOne(), | appliedFunction(rank_Relation_1___Window_1__T_1__Integer_1_, [var('p'), var('w'), var('r')]))
+ ]->getValue($r.function.body->toOne()->cast(@AppliedFunction).fControl->toOne())->eval();
+
+ let aggregation = lambda($func, [var('p'), var('w'), var('r')]);
+ createColSpec($name, $aggregation);,
+ a:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapAggregation[1] |
+ let mapFn = lambda(property($a.columnName, var('r')), [var('p'), var('w'), var('r')]);
+ let aggregation = $a.function->transform($extensions)->cast(@Lambda);
+ createColSpec($name, $mapFn, $aggregation);
+ ]),
+ a:AppliedFunction[1] |
+ [
+ pair(func_String_1__FunctionDefinition_1__TdsOlapAggregation_1_.name->toOne(), |
+ ^ClassInstance(_type='classInstance',
+ value = ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapAggregation(function = $a.parameters->at(1)->cast(@Lambda), columnName = $a.parameters->at(0)->cast(@CString).value),
+ type= 'tdsOlapAggregation')
+ ),
+ pair(func_FunctionDefinition_1__TdsOlapRank_1_.name->toOne(), |
+ ^ClassInstance(_type='classInstance',
+ value = ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapRank(function = $a.parameters->at(0)->cast(@Lambda)),
+ type= 'tdsOlapRank')
+ )
+ ]->getValue($a.fControl->toOne())->eval()->transformOlapFunction($name, $extensions),
+ l:Lambda[1] |
+ ^ClassInstance(_type='classInstance',
+ value = ^meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapRank(function = $l),
+ type= 'tdsOlapRank')->transformOlapFunction($name, $extensions)
+ ]);
+}
+
+function meta::pure::tds::toRelation::getOlapExtendFunction(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]):Function[1]
+{
+
+ $v->match([
+ c:ClassInstance[1] |
+ $c.value->match([
+ r:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapRank[1] | extend_Relation_1___Window_1__FuncColSpecArray_1__Relation_1_,
+ a:meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::TdsOlapAggregation[1] | extend_Relation_1___Window_1__AggColSpecArray_1__Relation_1_
+ ]),
+ l:Lambda[1] | extend_Relation_1___Window_1__FuncColSpecArray_1__Relation_1_
+ ]);
+}
+
+function meta::pure::tds::toRelation::transformJoinColsToFunc(left:String[*], right:String[*]):Lambda[1]
+{
+ let func = $left->zip($right)->map(v |
+ let lc = property($v.first, var('x'));
+ let rc = property($v.second, var('y'));
+
+ appliedFunction(equal_Any_MANY__Any_MANY__Boolean_1_, [$lc, $rc]);
+ )->potentiallyCombineToLogical(true);
+
+ lambda($func, [var('x'), var('y')]);
+}
+
+function meta::pure::tds::toRelation::transformSortInfo(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]):AppliedFunction[0..1]
+{
+ $v->match([
+ f:AppliedFunction[1] |
+ let column = $f.parameters->at(0)->cast(@CString).value->createColSpecInstance();
+ [
+ pair(asc_String_1__SortInformation_1_.name->toOne(),
+ | appliedFunction(ascending_ColSpec_1__SortInfo_1_, $column)),
+ pair(desc_String_1__SortInformation_1_.name->toOne(),
+ | appliedFunction(descending_ColSpec_1__SortInfo_1_, $column))
+ ]->getValue($f.fControl->toOne())->eval();,
+ s:ClassInstance[1] |
+ let si = $s.value->cast(@TDSSortInformation);
+ let column = createColSpecInstance($si.column);
+ [
+ pair('ASC',
+ | appliedFunction(ascending_ColSpec_1__SortInfo_1_, $column)),
+ pair('DESC',
+ | appliedFunction(descending_ColSpec_1__SortInfo_1_, $column))
+ ]->getValue($si.direction)->eval();,
+ v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[0..1] | []
+ ])
+}
+
+function meta::pure::tds::toRelation::transformJoinKind(v:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ $v->match([
+ a:AppliedProperty[1] | meta::relational::metamodel::join::JoinType->extractEnumValue($a.property);
+ ])->getJoinKind()->transformJoinKind();
+}
+
+function meta::pure::tds::toRelation::transformJoinKind(joinKind:meta::pure::functions::relation::JoinKind[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ processExtractEnumValue(meta::pure::functions::relation::JoinKind, $joinKind.name);
+}
+
+function meta::pure::tds::toRelation::getJoinKind(joinType:meta::relational::metamodel::join::JoinType[1]):meta::pure::functions::relation::JoinKind[1]
+{
+ [
+ pair(meta::relational::metamodel::join::JoinType.LEFT_OUTER, | meta::pure::functions::relation::JoinKind.LEFT),
+ pair(meta::relational::metamodel::join::JoinType.INNER, | meta::pure::functions::relation::JoinKind.INNER)
+ ]->getValue($joinType, | fail('unsupported join type ' + $joinType.name); meta::pure::functions::relation::JoinKind.INNER;)->eval();
+}
+
+
+function <> meta::pure::tds::toRelation::potentiallyCombineToLogical(expressions:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[*], and:Boolean[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+
+ $expressions->match([
+ e:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1] | $e,
+ e:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[2..*] | combineToLogical($e, $and),
+ e:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[0..1] | fail('must be at least 1 expression'); $expressions->toOne();
+ ])
+}
+
+function <> meta::pure::tds::toRelation::combineToLogical(expressions:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[2..*], and:Boolean[1]):meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::ValueSpecification[1]
+{
+ let func = if ($and, | and_Boolean_1__Boolean_1__Boolean_1_, | or_Boolean_1__Boolean_1__Boolean_1_);
+
+ $expressions->slice(2, $expressions->size())->fold({item, acc |
+ appliedFunction($func, [$item, $acc]);
+ },
+ appliedFunction($func, [$expressions->at(0), $expressions->at(1)])
+ );
+}
+
+function meta::pure::tds::toRelation::getValue(pairs : meta::pure::functions::collection::Pair[*], key : X[1]) : Y[1]
+{
+ let r = newMap($pairs)->get($key);
+ assert($r->isNotEmpty(), | 'No value found for ' + $key->makeString() + if($pairs->size() < 15 && ($key->instanceOf(String) || $key->instanceOf(Enumeration)), |', expected one of ' + $pairs.first->map(x|$x->makeString())->sort()->joinStrings('[', ',', ']'), |''));
+ $r->toOne();
+}
+
+function meta::pure::tds::toRelation::getValue(pairs : meta::pure::functions::collection::Pair[*], key : X[1], defaultValue : Y[1]) : Y[1]
+{
+ let r = newMap($pairs)->get($key);
+ if ($r->isEmpty(),
+ | $defaultValue,
+ | $r->toOne();
+ );
+}
+
+function meta::pure::tds::toRelation::resolveFControlFunction(a:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedFunction[1], extensions:meta::pure::extension::Extension[*]):Function[1]
+{
+ assert($a.fControl->isNotEmpty(), 'fControl required to calculate type');
+
+ let state = meta::protocols::pure::vX_X_X::transformation::fromPureGraph::toPureGrammar::defaultAlloyToPureState($extensions); //todo plug in extensions
+
+ $a->meta::protocols::pure::vX_X_X::transformation::fromPureGraph::toPureGrammar::resolveFControlFunction($state.funcs);
+}
diff --git a/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/testTdsToRelation.pure b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/testTdsToRelation.pure
new file mode 100644
index 00000000000..5eae6c1c5ab
--- /dev/null
+++ b/legend-engine-core/legend-engine-core-pure/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/tds/relation/testTdsToRelation.pure
@@ -0,0 +1,435 @@
+// Copyright 2025 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 meta::pure::extension::*;
+import meta::relational::metamodel::join::*;
+import meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::*;
+import meta::pure::tds::toRelation::*;
+
+Class meta::pure::tds::toRelation::TestClass
+{
+ string:String[1];
+ integer:Integer[1];
+ number:Number[1];
+ float:Float[1];
+ bool:Boolean[1];
+}
+
+
+function <> meta::pure::tds::toRelation::testProjectMulti():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project(
+ [
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ]
+ )
+ },
+ {| TestClass.all()->project(
+ [
+ x | $x.string,
+ x | $x.integer
+ ], [
+ 'str',
+ 'int'
+ ]
+ )
+ }
+ ],
+ {|
+ TestClass.all()->project(~[
+ str: x | $x.string,
+ int: x | $x.integer
+ ])
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testProjectSingle():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project(col(x | $x.string, 'str'))},
+ {| TestClass.all()->project(x | $x.string, 'str')}
+ ],
+ {| TestClass.all()->project(~[str: x | $x.string])}
+ );
+}
+
+//TODO activate when relation inference is fixed on project
+// function <> meta::pure::tds::toRelation::testTDSProjectSingle():Boolean[1]
+// {
+// test(
+// {| TestClass.all()
+// ->project(col(x | $x.string, 'str'))
+// ->project(col(x:TDSRow[1] | $x.getString('str')->toOne() + 'abc', 'newCol'))},
+// {| TestClass.all()
+// ->project(~[str: x | $x.string])
+// ->project(~[newCol : x | $x.str->toOne() + 'abc'])}
+// );
+// }
+
+// function <> meta::pure::tds::toRelation::testTDSProjectMulti():Boolean[1]
+// {
+// test(
+// {| TestClass.all()
+// ->project([col(x | $x.string, 'str'), col(x | $x.integer, 'int')])
+// ->project([
+// col(x:TDSRow[1] | $x.getString('str')->toOne() + 'abc', 'newCol'),
+// col(x:TDSRow[1] | $x.getString('str')->toOne() + 'def', 'newCol2')
+// ])},
+// {| TestClass.all()
+// ->project(~[str: x | $x.string, int: x | $x.integer])
+// ->project(~[
+// newCol : x | $x.str->toOne() + 'abc',
+// newCol2 : x | $x.str->toOne() + 'def'
+// ])
+// }
+// );
+// }
+
+
+function <> meta::pure::tds::toRelation::testExtendSingle():Boolean[1]
+{
+ test(
+ {| TestClass.all()
+ ->project(col(x | $x.string, 'str'))
+ ->extend(
+ col(x:TDSRow[1] | $x.getString('str')->toOne() + 'abc', 'col')
+ )
+ },
+ {| TestClass.all()
+ ->project(~[str: x | $x.string])
+ ->extend(~[
+ col: x | $x.str->toOne() + 'abc'
+ ])
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testExtendMulti():Boolean[1]
+{
+ test(
+ {| TestClass.all()
+ ->project(col(x | $x.string, 'str'))
+ ->extend([
+ col(x:TDSRow[1] | $x.getString('str')->toOne() + 'abc', 'col1'),
+ col(x:TDSRow[1] | $x.getString('str')->toOne() + 'def', 'col2')
+ ])
+ },
+ {| TestClass.all()
+ ->project(~[str: x | $x.string])
+ ->extend(~[
+ col1: x | $x.str->toOne() + 'abc',
+ col2: x | $x.str->toOne() + 'def'
+ ])
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testGroupAllMulti():Boolean[1]
+{
+ test(
+ {| TestClass.all()->groupBy(
+ [
+ x | $x.string,
+ x | $x.bool
+ ],
+ [
+ agg(x | $x.integer, y | $y->sum()),
+ agg(x | $x.float, y | $y->count())
+ ],
+ [
+ 'str', 'boolean', 'sum', 'count'
+ ]
+ )
+ },
+ {|
+ TestClass.all()->project(~[
+ str : x | $x.string,
+ boolean : x | $x.bool,
+ sum : x | $x.integer,
+ count : x | $x.float
+ ])->groupBy(
+ ~[str, boolean],
+ ~[
+ sum : x | $x.sum : y | $y->sum(),
+ count : x | $x.count : y | $y->count()
+ ]
+ )
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testGroupBySingle():Boolean[1]
+{
+ test(
+ {| TestClass.all()->groupBy(x | $x.string, agg(x | $x.integer, y | $y->sum()), ['str', 'sum'])},
+ {| TestClass.all()->project(~[str : x | $x.string, sum : x | $x.integer])->groupBy(~[str], ~[sum : x | $x.sum : y | $y->sum()])}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testTDSGroupBy():Boolean[1]
+{
+ test(
+ {| TestClass.all()
+ ->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int'),
+ col(x | $x.float, 'float')
+ ])
+ ->groupBy(
+ ['str', 'float'],
+ [
+ agg('sum', row | $row.getInteger('int'), y | $y->sum()),
+ agg('max', row | $row.getInteger('int'), y | $y->max())
+ ])
+ },
+ {| TestClass.all()
+ ->project(~[
+ str: x | $x.string,
+ int: x | $x.integer,
+ float: x | $x.float
+ ])
+ ->groupBy(
+ ~[str, float],
+ ~[
+ sum : row | $row.int : y | $y->sum(),
+ max : row | $row.int : y | $y->max()
+ ]
+ )
+ }
+ );
+}
+
+
+function <> meta::pure::tds::toRelation::testSortSingle():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->sort(asc('str'))},
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->sort(^SortInformation(column = 'str', direction = SortDirection.ASC))}
+ ],
+ {| TestClass.all()->project(~[str: x | $x.string])->sort(ascending(~str))}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testDistinct():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->distinct()},
+ {| TestClass.all()->project(~[str: x | $x.string])->distinct()}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testTakeLimit():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->limit(1)},
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->take(1)}
+ ],
+ {| TestClass.all()->project(~[str: x | $x.string])->limit(1)}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testSlice():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->slice(1, 2)},
+ {| TestClass.all()->project(~[str: x | $x.string])->slice(1, 2)}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testDrop():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->drop(1)},
+ {| TestClass.all()->project(~[str: x | $x.string])->drop(1)}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testConcatenate():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->concatenate(TestClass.all()->project(col(x | $x.float, 'str')))},
+ {| TestClass.all()->project(~[str: x | $x.string])->concatenate(TestClass.all()->project(~[str: x | $x.float]))}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testFilter():Boolean[1]
+{
+ test(
+ {| TestClass.all()
+ ->filter(x | $x.string == 'abc')
+ ->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ])
+ ->filter(x |
+ ($x.getString('str') == 'abc') || ($x.getString('str') == 'def')
+ &&
+ ($x.getInteger('int')->toOne()->in([1, $x.getInteger('int')->toOne()]))
+ )
+ },
+ {| TestClass.all()
+ ->filter(x | $x.string == 'abc')
+ ->project(~[
+ str: x | $x.string,
+ int: x | $x.integer
+ ])
+ ->filter(x |
+ ($x.str == 'abc') || ($x.str == 'def')
+ &&
+ ($x.int->toOne()->in([1, $x.int->toOne()]))
+ )
+ }
+ );
+}
+
+
+function <> meta::pure::tds::toRelation::testRenameSingle():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->renameColumns(pair('str', 'STR'))},
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->renameColumns(^meta::pure::functions::collection::Pair(first = 'str', second ='STR'))},
+ {| TestClass.all()->project(col(x | $x.string, 'str'))->renameColumn('str', 'STR')}
+ ],
+ {| TestClass.all()->project(~[str: x | $x.string])->rename(~str, ~STR)}
+ );
+}
+
+function <> meta::pure::tds::toRelation::testRenameMulti():Boolean[1]
+{
+ test(
+ [
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ])->renameColumns([
+ pair('str', 'STR'),
+ pair('int', 'INT')
+ ])}
+ ],
+ {| TestClass.all()
+ ->project(~[str: x | $x.string, int: x | $x.integer])
+ ->rename(~str, ~STR)
+ ->rename(~int, ~INT)
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testRestrictDistinct():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ])->restrictDistinct('int')
+ },
+ {| TestClass.all()
+ ->project(~[str: x | $x.string, int: x | $x.integer])
+ ->select(~[int])
+ ->distinct()
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testRestrictSingle():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ])->restrict('int')
+ },
+ {| TestClass.all()
+ ->project(~[str: x | $x.string, int: x | $x.integer])
+ ->select(~[int])
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testRestrictMulti():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.integer, 'int')
+ ])->restrict(['int', 'str'])
+ },
+ {| TestClass.all()
+ ->project(~[str: x | $x.string, int: x | $x.integer])
+ ->select(~[int, str])
+ }
+ );
+}
+
+function <> meta::pure::tds::toRelation::testOlap():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str'),
+ col(x | $x.number, 'int')
+ ])->olapGroupBy(['str'], asc('int'), y | $y->meta::pure::functions::math::olap::rowNumber(), 'ROW')
+ ->olapGroupBy(['str'], desc('int'), func(y | $y->meta::pure::functions::math::olap::denseRank()), 'DENSE RANK')
+ ->olapGroupBy(['str'], asc('int'), func(y | $y->meta::pure::functions::math::olap::rank()), 'RANK')
+ ->olapGroupBy(['str'], [], func('int', y | $y->max()), 'MAX')
+ ->olapGroupBy(['str'], func('int', y | $y->max()), 'MAX2')
+ ->olapGroupBy(func('int', y|$y->min()),'MIN')
+ ->olapGroupBy(asc('int'),func('int', y|$y->max()),'MAX3')
+ ->olapGroupBy(['int'], y|$y->meta::pure::functions::math::olap::rank(),'RANK2')
+ ->olapGroupBy(asc('int'), y|$y->meta::pure::functions::math::olap::rank(),'RANK3')
+ },
+
+ {| TestClass.all()
+ ->project(~[str: x | $x.string, int: x | $x.number])
+ ->extend(over(~[str], [~int->ascending()]), ~[ROW:{p,w,r| $p->rowNumber($r)}])
+ ->extend(over(~[str], [~int->descending()]), ~['DENSE RANK':{p,w,r| $p->denseRank($w, $r)}])
+ ->extend(over(~[str], [~int->ascending()]), ~[RANK:{p,w,r| $p->rank($w, $r)}])
+ ->extend(over(~[str], []), ~[MAX:{p,w,r|$r.int}:y|$y->max()])
+ ->extend(over(~[str]), ~[MAX2:{p,w,r|$r.int}:y|$y->max()])
+ ->extend(over([], [], []), ~[MIN:{p,w,r|$r.int}:y|$y->min()])
+ ->extend(over(ascending(~int)), ~[MAX3:{p,w,r|$r.int}:y|$y->max()])
+ ->extend(over(~[int]), ~[RANK2:{p,w,r| $p->rank($w, $r)}])
+ ->extend(over([~int->ascending()]), ~[RANK3:{p,w,r| $p->rank($w, $r)}])
+ }
+ )
+}
+
+function meta::pure::tds::toRelation::test(inputs:LambdaFunction[*], expected:LambdaFunction[1]):Boolean[1]
+{
+ test($inputs, $expected, [])
+}
+
+function meta::pure::tds::toRelation::test(inputs:LambdaFunction[*], expected:LambdaFunction[1], extensions:Extension[*]):Boolean[1]
+{
+ $inputs->forAll(input |
+ let transformed = $input->meta::pure::tds::toRelation::transform($extensions);
+ assertLambdaJSONEquals($expected, $transformed);
+ )
+}
+
+function meta::pure::tds::toRelation::assertLambdaJSONEquals(expected:FunctionDefinition[1], actual:Lambda[1]): Boolean[1]
+{
+ assertEquals($expected->functionJSON(), $actual->meta::json::toJSON(100));
+}
+
+function meta::pure::tds::toRelation::functionJSON(func:FunctionDefinition[1]): String[1]
+{
+ $func->meta::protocols::pure::vX_X_X::transformation::fromPureGraph::transformLambda([])->meta::json::toJSON(100);
+}
diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/extensions/extension.pure b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/extensions/extension.pure
index f739960c152..b58d06291a2 100644
--- a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/extensions/extension.pure
+++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/extensions/extension.pure
@@ -155,6 +155,7 @@ function meta::relational::extension::relationalExtension() : meta::pure::extens
let varMultiplicity = if($parameterVal->instanceOf(List), |ZeroMany, |PureOne);
convertPlaceHolderToSQLString(^meta::relational::functions::pureToSqlQuery::metamodel::VarPlaceHolder(name = $name, type=$type, multiplicity=$varMultiplicity), $dbConfig.dbExtension.literalProcessor, $d.timeZone);
},
+ tdsToRelation = meta::pure::tds::toRelation::tdsToRelationExtension(),
tdsSchema_resolveSchemaImpl = {fe:FunctionExpression[1], openVars:Map>[1], extensions:Extension[*]|
[
join_TabularDataSet_1__TabularDataSet_1__JoinType_1__String_$1_MANY$__String_$1_MANY$__TabularDataSet_1_,
diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/tdsToRelation.pure b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/tdsToRelation.pure
new file mode 100644
index 00000000000..0498a21cd14
--- /dev/null
+++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/tdsToRelation.pure
@@ -0,0 +1,58 @@
+// Copyright 2025 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 meta::protocols::pure::vX_X_X::transformation::helpers::*;
+import meta::protocols::pure::vX_X_X::metamodel::valueSpecification::raw::*;
+import meta::pure::extension::*;
+import meta::pure::tds::toRelation::*;
+
+function meta::pure::tds::toRelation::tdsToRelationExtension():TdsToRelationExtension_V_X_X[1]
+ {
+ ^TdsToRelationExtension_V_X_X(
+ transfers = {a:meta::protocols::pure::vX_X_X::metamodel::m3::valuespecification::AppliedFunction[1], extensions:Extension[*] |
+ [
+ pair(join_TabularDataSet_1__TabularDataSet_1__JoinType_1__String_$1_MANY$__TabularDataSet_1_->cast(@Function),
+ |
+ let left = $a.parameters->at(0)->transform($extensions);
+ let right = $a.parameters->at(1)->transform($extensions);
+ let joinKind = $a.parameters->at(2)->transformJoinKind();
+ let joinCols = $a.parameters->at(3)->fromCollection()->cast(@CString).value;
+ let func = transformJoinColsToFunc($joinCols, $joinCols);
+
+ appliedFunction(join_Relation_1__Relation_1__JoinKind_1__Function_1__Relation_1_, [$left, $right, $joinKind, $func]);
+ ),
+ pair(join_TabularDataSet_1__TabularDataSet_1__JoinType_1__String_$1_MANY$__String_$1_MANY$__TabularDataSet_1_->cast(@Function),
+ |
+ let left = $a.parameters->at(0)->transform($extensions);
+ let right = $a.parameters->at(1)->transform($extensions);
+ let joinKind = $a.parameters->at(2)->transformJoinKind();
+ let leftJoinCols = $a.parameters->at(3)->fromCollection()->cast(@CString).value;
+ let rightJoinCols = $a.parameters->at(4)->fromCollection()->cast(@CString).value;
+ let func = transformJoinColsToFunc($leftJoinCols, $rightJoinCols);
+
+ appliedFunction(join_Relation_1__Relation_1__JoinKind_1__Function_1__Relation_1_, [$left, $right, $joinKind, $func]);
+ ),
+ pair(join_TabularDataSet_1__TabularDataSet_1__JoinType_1__Function_1__TabularDataSet_1_->cast(@Function),
+ |
+ let left = $a.parameters->at(0)->transform($extensions);
+ let right = $a.parameters->at(1)->transform($extensions);
+ let joinKind = $a.parameters->at(2)->transformJoinKind();
+ let func = $a.parameters->at(3)->transform($extensions);
+
+ appliedFunction(join_Relation_1__Relation_1__JoinKind_1__Function_1__Relation_1_, [$left, $right, $joinKind, $func]);
+ )
+ ]
+ }
+ )
+ }
diff --git a/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/testTdsToRelation.pure b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/testTdsToRelation.pure
new file mode 100644
index 00000000000..4a7aedeb643
--- /dev/null
+++ b/legend-engine-xts-relationalStore/legend-engine-xt-relationalStore-generation/legend-engine-xt-relationalStore-pure/legend-engine-xt-relationalStore-core-pure/src/main/resources/core_relational/relational/tds/relation/testTdsToRelation.pure
@@ -0,0 +1,64 @@
+// Copyright 2025 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 meta::relational::metamodel::join::*;
+import meta::pure::tds::toRelation::*;
+
+function <> meta::pure::tds::toRelation::testJoinUsing():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str1'),
+ col(x | $x.integer, 'int1')
+ ])->join(TestClass.all()->project([
+ col(x | $x.string, 'str2'),
+ col(x | $x.integer, 'int2')
+ ]), JoinType.INNER, ['int1', 'str1'], ['int2', 'str2']
+ )
+ },
+
+ {| TestClass.all()
+ ->project(~[str1: x | $x.string, int1: x | $x.integer])
+ ->join(TestClass.all()
+ ->project(~[str2: x | $x.string, int2: x | $x.integer]),
+ JoinKind.INNER, {x, y | ($x.int1 == $y.int2) && $x.str1 == $y.str2}
+ )
+ },
+ meta::relational::extension::relationalExtension()
+ )
+}
+
+function <> meta::pure::tds::toRelation::testJoinFunc():Boolean[1]
+{
+ test(
+ {| TestClass.all()->project([
+ col(x | $x.string, 'str1'),
+ col(x | $x.integer, 'int1')
+ ])->join(TestClass.all()->project([
+ col(x | $x.string, 'str2'),
+ col(x | $x.integer, 'int2')
+ ]), JoinType.INNER, {x, y | $x.getInteger('int1') == $y.getInteger('int2')}
+ )
+ },
+
+ {| TestClass.all()
+ ->project(~[str1: x | $x.string, int1: x | $x.integer])
+ ->join(TestClass.all()
+ ->project(~[str2: x | $x.string, int2: x | $x.integer]),
+ JoinKind.INNER, {x, y | $x.int1 == $y.int2}
+ )
+ },
+ meta::relational::extension::relationalExtension()
+ )
+}