Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support mapping relations to classes #3306

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public Client(MutableList<ReplExtension> replExtensions, MutableList<CompleterEx
new Debug(this),
new Doc(this),
new Graph(this),
new PromoteRelationToModel(this),
new Execute(this)
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.MutableList;
import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel;
import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
import org.finos.legend.engine.repl.core.ReplExtension;
import org.finos.legend.engine.repl.core.legend.LegendInterface;
Expand All @@ -25,7 +26,7 @@ public class ModelState
{
private final LegendInterface legendInterface;
private final MutableList<ReplExtension> replExtensions;
private MutableList<String> state = Lists.mutable.empty();
private final MutableList<String> state = Lists.mutable.empty();

public ModelState(LegendInterface legendInterface, MutableList<ReplExtension> replExtensions)
{
Expand All @@ -44,6 +45,16 @@ public PureModelContextData parse()
return this.legendInterface.parse(getText());
}

public PureModel compile()
{
return this.legendInterface.compile(parse());
}

public PureModel compileWithTransient(String transientCode)
{
return this.legendInterface.compile(parseWithTransient(transientCode));
}

public PureModelContextData parseWithTransient(String transientCode)
{
return this.legendInterface.parse(getText() + transientCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// 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.

package org.finos.legend.engine.repl.core.commands;

import org.eclipse.collections.api.list.MutableList;
import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel;
import org.finos.legend.engine.language.pure.compiler.toPureGraph.SourceInformationHelper;
import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType;
import org.finos.legend.engine.repl.client.Client;
import org.finos.legend.engine.repl.core.Command;
import org.finos.legend.engine.repl.shared.RelationClassMappingGenerator;
import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException;
import org.finos.legend.pure.generated.core_pure_corefunctions_stringExtension;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.FunctionDefinition;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.relation.RelationType;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.generics.GenericType;
import org.finos.legend.pure.m3.navigation.M3Paths;
import org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement;
import org.finos.legend.pure.m3.navigation.function.FunctionDescriptor;
import org.finos.legend.pure.m3.navigation.relation._RelationType;
import org.finos.legend.pure.runtime.java.compiled.execution.CompiledExecutionSupport;
import org.jline.reader.Candidate;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;

public class PromoteRelationToModel implements Command
{
private final Client client;

public PromoteRelationToModel(Client client)
{
this.client = client;
}

@Override
public boolean process(String line) throws Exception
{
if (line.startsWith("promoteRelationToModel"))
{
String[] tokens = line.split(" ");
if (tokens.length < 1 || tokens.length > 3)
{
throw new RuntimeException("Command should be used as '" + this.documentation() + "'");
}

PureModel currentModel = client.getModelState().compile();
CompiledExecutionSupport es = currentModel.getExecutionSupport();
String relationName = core_pure_corefunctions_stringExtension.Root_meta_pure_functions_string_makeCamelCase_String_1__Boolean_1__String_1_(tokens[1], true, es);
String packagePrefix = tokens[2];

String className = packagePrefix + "::domain::" + relationName + "Class";
String functionName = packagePrefix + "::function::" + relationName + "Fn";
String mappingName = packagePrefix + "::mapping::" + relationName + "Mapping";

if (currentModel.getMapping_safe(mappingName) != null)
{
throw new EngineException("Mapping by name " + mappingName + "already exists in current state!");
}

String expression = this.client.getLastCommand(1);
if (expression == null)
{
this.client.printError("Failed to retrieve the last command");
return true;
}

try
{
// generate initial function with return type Any, then resolve type post compilation
FunctionDefinition<?> relationFunction = getFunction(RelationClassMappingGenerator.generateFunction(functionName, expression), functionName);
RelationType<?> relationType = getRelationType(relationFunction, es);
String returnType = M3Paths.Relation + "<" + _RelationType.print(relationType, es.getProcessorSupport()) + ">";
String functionBody = RelationClassMappingGenerator.generateFunction(functionName, expression, returnType);

String classBody = RelationClassMappingGenerator.generateClass(className, relationType, es);

String functionDescriptor = FunctionDescriptor.writeFunctionDescriptor(new StringBuilder(), getFunction(functionBody, functionName), false, es.getProcessorSupport()).toString();
String mappingBody = RelationClassMappingGenerator.generateClassMapping(mappingName, className, functionDescriptor, relationType, es);

client.getModelState().addElement(mappingBody);
client.getModelState().addElement(classBody);
client.getModelState().addElement(functionBody);
// Verify generated code compiles
client.getModelState().compile();
return true;
}
catch (Exception e)
{
this.client.printError("Last command run may not have been an execution of a Pure expression which returns a Relation (command run: '" + expression + "')");
if (e instanceof EngineException)
{
this.client.printEngineError((EngineException) e, expression);
}
else
{
throw e;
}
}
}
return false;
}

private FunctionDefinition<?> getFunction(String functionBody, String functionName)
{
PureModel model = this.client.getModelState().compileWithTransient(functionBody);
return model.getAllPackageableElementsOfType(ConcreteFunctionDefinition.class).detect(f -> PackageableElement.getUserPathForPackageableElement(f).contains(functionName));
}

private static RelationType<?> getRelationType(FunctionDefinition<?> relationFunction, CompiledExecutionSupport es)
{
GenericType lastExpressionType = relationFunction._expressionSequence().toList().getLast()._genericType();
if (!es.getProcessorSupport().type_subTypeOf(lastExpressionType._rawType(), es.getProcessorSupport().package_getByUserPath(M3Paths.Relation)))
{
throw new EngineException("Last expression returned a " + org.finos.legend.pure.m3.navigation.generictype.GenericType.print(lastExpressionType, es.getProcessorSupport()) + ". A Relation is required for promotion to class.", SourceInformationHelper.fromM3SourceInformation(relationFunction.getSourceInformation()), EngineErrorType.COMPILATION);
}
return (RelationType<?>) lastExpressionType._typeArguments().toList().getFirst()._rawType();
}

@Override
public String documentation()
{
return "promoteRelationToModel <relation name> <package prefix>";
}

@Override
public String description()
{
return "generate model class and corresponding relation expression mapping for last run relation expression. Example Usage: promoteRelationToModel AggregatedPerson my::package";
}

@Override
public MutableList<Candidate> complete(String cmd, LineReader lineReader, ParsedLine parsedLine)
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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.

package org.finos.legend.engine.repl.shared;

import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposer;
import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerContext;
import org.finos.legend.engine.protocol.pure.v1.model.PackageableElement;
import org.finos.legend.engine.protocol.pure.v1.model.context.PackageableElementPointer;
import org.finos.legend.engine.protocol.pure.v1.model.context.PackageableElementType;
import org.finos.legend.engine.protocol.pure.v1.model.domain.Class;
import org.finos.legend.engine.protocol.pure.v1.model.domain.Property;
import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.Mapping;
import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.PropertyMapping;
import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.PropertyPointer;
import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.relationFunction.RelationFunctionClassMapping;
import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.relationFunction.RelationFunctionPropertyMapping;
import org.finos.legend.engine.protocol.pure.v1.model.type.PackageableType;
import org.finos.legend.pure.generated.core_pure_corefunctions_stringExtension;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.multiplicity.Multiplicity;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.relation.Column;
import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.relation.RelationType;
import org.finos.legend.pure.m3.execution.ExecutionSupport;
import org.finos.legend.pure.m3.navigation.relation._Column;
import org.finos.legend.pure.runtime.java.compiled.execution.CompiledExecutionSupport;

import java.util.Collections;

public class RelationClassMappingGenerator
{
private static String getColumnPureType(Column<?, ?> c)
{
return _Column.getColumnType(c)._rawType()._name();
}

private static String getPropertyName(String columnName, ExecutionSupport es)
{
String sqlSafeColumnName = columnName.replaceAll("[ /@~<>.]", "_").replace("\"", "");
return core_pure_corefunctions_stringExtension.Root_meta_pure_functions_string_makeCamelCase_String_1__String_1_(sqlSafeColumnName, es);
}

public static String generateFunction(String functionName, String expression)
{
return generateFunction(functionName, expression, "Any", "1");
}

public static String generateFunction(String functionName, String expression, String returnType)
{
return generateFunction(functionName, expression, returnType, "1");
}

private static String generateFunction(String functionName, String expression, String returnType, String returnMultiplicity)
{
return "###Pure\n" +
"function " + functionName + "():" + returnType + "[" + returnMultiplicity + "]\n" +
"{\n" +
expression + ";\n" +
"}\n";
}

public static String generateClass(String className, RelationType<?> relationType, CompiledExecutionSupport es)
{
Class clazz = new Class();
clazz.name = className;
clazz.properties = relationType._columns().collect(c ->
{
Property property = new Property();
property.name = getPropertyName(c._name(), es);
property.genericType = new org.finos.legend.engine.protocol.pure.v1.model.type.GenericType();
property.genericType.rawType = new PackageableType(getColumnPureType(c));
Multiplicity m = _Column.getColumnMultiplicity(c);
property.multiplicity = new org.finos.legend.engine.protocol.pure.v1.model.domain.Multiplicity(m._lowerBound()._value().intValue(), m. _upperBound()._value().intValue());
return property;
}).toList();
return "###Pure\n" + renderElement(clazz);
}

public static String generateClassMapping(String mappingName, String className, String functionDescriptor, RelationType<?> relationType, CompiledExecutionSupport es)
{
Mapping mapping = new Mapping();
mapping.name = mappingName;
RelationFunctionClassMapping classMapping = new RelationFunctionClassMapping();
classMapping.relationFunction = new PackageableElementPointer();
classMapping.relationFunction.type = PackageableElementType.FUNCTION;
classMapping.relationFunction.path = functionDescriptor;
classMapping._class = className;
classMapping.propertyMappings = relationType._columns().collect(c ->
{
RelationFunctionPropertyMapping propertyMapping = new RelationFunctionPropertyMapping();
propertyMapping.property = new PropertyPointer();
propertyMapping.property._class = className;
propertyMapping.property.property = getPropertyName(c._name(), es);
propertyMapping.column = c._name();
return (PropertyMapping) propertyMapping;
}).toList();
mapping.classMappings = Collections.singletonList(classMapping);
return "###Mapping\n" + renderElement(mapping);
}

private static String renderElement(PackageableElement element)
{
return PureGrammarComposer.newInstance(PureGrammarComposerContext.Builder.newInstance().build()).render(element);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static Test suite() throws Exception
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::merge", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::multigrain", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::propertyfunc", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::relation", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::selfJoin", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::sqlFunction", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
suite.addTest(buildSuite(TestCollection.collectTests("meta::relational::tests::mapping::subType", executionSupport.getProcessorSupport(), ci -> satisfiesConditions(ci, executionSupport.getProcessorSupport())), executionSupport));
Expand Down
Loading
Loading