diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecute.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecute.java index f6e3dab9acf..a6748dcd590 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecute.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecute.java @@ -25,7 +25,6 @@ import org.eclipse.collections.api.block.function.Function; import org.eclipse.collections.api.factory.Maps; import org.eclipse.collections.api.factory.Lists; -import org.eclipse.collections.api.factory.Sets; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.map.MutableMap; import org.eclipse.collections.api.tuple.Pair; @@ -38,6 +37,7 @@ import org.finos.legend.engine.generation.dataquality.DataQualitySampleValuesLambdaGenerator; import org.finos.legend.engine.language.pure.compiler.toPureGraph.HelperValueSpecificationBuilder; import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.RelationTypeHelper; import org.finos.legend.engine.language.pure.grammar.from.PureGrammarParser; import org.finos.legend.engine.language.pure.modelManager.ModelManager; import org.finos.legend.engine.language.pure.modelManager.sdlc.configuration.MetaDataServerConfiguration; @@ -80,6 +80,7 @@ import org.finos.legend.pure.generated.Root_meta_pure_extension_Extension; import org.finos.legend.pure.generated.core_dataquality_generation_dataquality; import org.finos.legend.pure.generated.core_dataquality_generation_datarecon; +import org.finos.legend.pure.generated.core_dataquality_generation_lambda_relation_type; import org.finos.legend.pure.generated.core_dataquality_generation_rule_suggestions; import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.PackageableElement; import org.finos.legend.pure.runtime.java.compiled.generation.processors.support.map.PureMap; @@ -548,6 +549,35 @@ public Response lambda(@Context HttpServletRequest request, DataQualityExecuteTr } + @POST + @Path("relationType") + @ApiOperation(value = "Get the relation type (output columns) of the engine-generated lambda for a DQ element. Dispatches by element type at the given package path: DataQualityRelationValidation uses the breaks lambda; DataQualityRelationComparison uses the reconciliation lambda.") + @Consumes({MediaType.APPLICATION_JSON, APPLICATION_ZLIB}) + @Produces(MediaType.APPLICATION_JSON) + public Response relationType(DataQualityExecuteTrialInput dataQualityExecuteInput, @ApiParam(hidden = true) @Pac4JProfileManager() ProfileManager pm) + { + MutableList profiles = ProfileManagerHelper.extractProfiles(pm); + Identity identity = Identity.makeIdentity(profiles); + long start = System.currentTimeMillis(); + LOGGER.info(new LogInfo(identity.getName(), DataQualityExecutionLoggingEventType.DATAQUALITY_RELATION_TYPE_START).toString()); + try (Scope scope = GlobalTracer.get().buildSpan("DataQuality: relationType").startActive(true)) + { + PureModel pureModel = this.modelManager.loadModel(dataQualityExecuteInput.model, dataQualityExecuteInput.clientVersion, identity, null); + PackageableElement packageableElement = pureModel.getPackageableElement(dataQualityExecuteInput.packagePath); + org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.relation.RelationType pureRelationType = + core_dataquality_generation_lambda_relation_type.Root_meta_external_dataquality_lambdaRelationType_getLambdaRelationType_PackageableElement_1__Function_1__RelationType_1_( + packageableElement, DataQualityProfilingLambdaGenerator.getLambdaCompiler(pureModel), pureModel.getExecutionSupport()); + org.finos.legend.engine.protocol.pure.m3.relation.RelationType relationType = + RelationTypeHelper.convert(pureRelationType, pureModel); + LOGGER.info(new LogInfo(identity.getName(), DataQualityExecutionLoggingEventType.DATAQUALITY_RELATION_TYPE_END, System.currentTimeMillis() - start).toString()); + return ManageConstantResult.manageResult(identity.getName(), relationType, objectMapper); + } + catch (Exception ex) + { + return ExceptionTool.exceptionManager(ex, LoggingEventType.GENERATE_PLAN_ERROR, identity.getName()); + } + } + @POST @Path("executeArtifacts") @Consumes({MediaType.APPLICATION_JSON, APPLICATION_ZLIB}) diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecutionLoggingEventType.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecutionLoggingEventType.java index 2400c62a6d6..4f74182fc74 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecutionLoggingEventType.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-api/src/main/java/org/finos/legend/engine/language/dataquality/api/DataQualityExecutionLoggingEventType.java @@ -35,5 +35,7 @@ public enum DataQualityExecutionLoggingEventType implements ILoggingEventType DATAQUALITY_RELATION_QUERY_COUNT_PLAN_EXECUTION_START, DATAQUALITY_RELATION_QUERY_COUNT_PLAN_EXECUTION_END, DATAQUALITY_PLAN_GENERATION_DEBUG_START, - DATAQUALITY_PLAN_GENERATION_DEBUG_STOP + DATAQUALITY_PLAN_GENERATION_DEBUG_STOP, + DATAQUALITY_RELATION_TYPE_START, + DATAQUALITY_RELATION_TYPE_END } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/DataQualityCompilerExtension.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/DataQualityCompilerExtension.java index 134fa46a69c..2cc7740f07e 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/DataQualityCompilerExtension.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/DataQualityCompilerExtension.java @@ -16,6 +16,7 @@ import org.eclipse.collections.api.RichIterable; import org.eclipse.collections.api.block.function.Function; +import org.eclipse.collections.api.block.function.Function3; import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.Sets; import org.eclipse.collections.api.list.ListIterable; @@ -23,11 +24,14 @@ import org.eclipse.collections.api.set.ImmutableSet; import org.eclipse.collections.impl.list.mutable.FastList; import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.data.EmbeddedDataFirstPassBuilder; import org.finos.legend.engine.language.pure.compiler.toPureGraph.extension.CompilerExtension; import org.finos.legend.engine.language.pure.compiler.toPureGraph.extension.Processor; import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.FunctionExpressionBuilderRegistrationInfo; import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.Handlers; import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.inference.ParametersInference; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.TestFirstPassBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.assertion.TestAssertionFirstPassBuilder; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQuality; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityExecutionContext; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityPersistenceStrategy; @@ -40,6 +44,10 @@ import org.finos.legend.engine.protocol.dataquality.metamodel.MappingAndRuntimeDataQualityExecutionContext; import org.finos.legend.engine.protocol.dataquality.metamodel.ReconStrategy; import org.finos.legend.engine.protocol.dataquality.metamodel.RelationValidation; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.PropertyGraphFetchTree; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.SubTypeGraphFetchTree; import org.finos.legend.engine.protocol.pure.m3.multiplicity.Multiplicity; @@ -49,15 +57,29 @@ 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.packageableElement.dataSpace.DataSpace; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.Mapping; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.runtime.PackageableRuntime; +import org.finos.legend.engine.protocol.pure.v1.model.test.Test; import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQuality; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityStoreTestData; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityStoreTestData_Impl; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityExecutionContext; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityPropertyGraphFetchTree; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityPropertyGraphFetchTree_Impl; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidation_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTest_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTestData; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTestData_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTestSuite; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTestSuite_Impl; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparison_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTest_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTestData; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTestData_Impl; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite_Impl; import org.finos.legend.pure.generated.Root_meta_external_dataquality_MD5HashStrategy_Impl; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRootGraphFetchTree; import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRootGraphFetchTree_Impl; @@ -81,6 +103,7 @@ import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.FunctionType; import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.Type; import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.generics.GenericType; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; import org.finos.legend.pure.m3.navigation._class._Class; import org.finos.legend.pure.m4.coreinstance.CoreInstance; import org.finos.legend.pure.m4.coreinstance.SourceInformation; @@ -207,6 +230,7 @@ private Processor getDataQualityRelationValidatio { metamodel._persistenceStrategy(buildPersistenceStrategy(dataqualityRelationValidation.persistenceStrategy, compileContext)); } + processDataQualityTestSuites(dataqualityRelationValidation.testSuites, metamodel, dataqualityRelationValidation.sourceInformation, compileContext); metamodel._validate(true, SourceInformationHelper.toM3SourceInformation(dataqualityRelationValidation.sourceInformation), compileContext.getExecutionSupport()); }, this::dataQualityRelationValidationPrerequisiteElementsPass @@ -310,6 +334,8 @@ private Processor getDataQualityRelationCompariso metamodel._expectedMatch(relationComparison.expectedMatch); } + processDataQualityTestSuites(relationComparison.testSuites, metamodel, relationComparison.sourceInformation, compileContext); + metamodel._validate(true, SourceInformationHelper.toM3SourceInformation(relationComparison.sourceInformation), compileContext.getExecutionSupport()); }, this::dataQualityRelationComparisonPrerequisiteElementsPass @@ -669,4 +695,185 @@ public List>> )); } + private static void processDataQualityTestSuites(List testSuites, Root_meta_external_dataquality_DataQualityRelationValidation_Impl metamodel, org.finos.legend.engine.protocol.pure.m3.SourceInformation elementSourceInformation, CompileContext compileContext) + { + if (testSuites == null || testSuites.isEmpty()) + { + return; + } + List testSuiteIds = ListIterate.collect(testSuites, s -> s.id); + List duplicateTestSuiteIds = testSuiteIds.stream().filter(e -> Collections.frequency(testSuiteIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateTestSuiteIds.isEmpty()) + { + throw new EngineException("Multiple testSuites found with ids : '" + String.join(",", duplicateTestSuiteIds) + "'", elementSourceInformation, EngineErrorType.COMPILATION); + } + ProcessingContext processingContext = new ProcessingContext("DataQualityRelationValidation '" + org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement.getUserPathForPackageableElement(metamodel) + "' Test Suites"); + metamodel._tests(ListIterate.collect(testSuites, suite -> + { + Root_meta_external_dataquality_DataQualityRelationValidationTestSuite pureSuite = (Root_meta_external_dataquality_DataQualityRelationValidationTestSuite) suite.accept(new TestFirstPassBuilder(compileContext, processingContext)); + pureSuite._testable(metamodel); + for (Root_meta_pure_test_AtomicTest pureTest : pureSuite._tests()) + { + pureTest._testable(metamodel); + } + return pureSuite; + })); + } + + private static void processDataQualityTestSuites(List testSuites, Root_meta_external_dataquality_DataQualityRelationComparison_Impl metamodel, org.finos.legend.engine.protocol.pure.m3.SourceInformation elementSourceInformation, CompileContext compileContext) + { + if (testSuites == null || testSuites.isEmpty()) + { + return; + } + List testSuiteIds = ListIterate.collect(testSuites, s -> s.id); + List duplicateTestSuiteIds = testSuiteIds.stream().filter(e -> Collections.frequency(testSuiteIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateTestSuiteIds.isEmpty()) + { + throw new EngineException("Multiple testSuites found with ids : '" + String.join(",", duplicateTestSuiteIds) + "'", elementSourceInformation, EngineErrorType.COMPILATION); + } + ProcessingContext processingContext = new ProcessingContext("DataQualityRelationComparison '" + org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement.getUserPathForPackageableElement(metamodel) + "' Test Suites"); + metamodel._tests(ListIterate.collect(testSuites, suite -> + { + Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite pureSuite = (Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite) suite.accept(new TestFirstPassBuilder(compileContext, processingContext)); + pureSuite._testable(metamodel); + for (Root_meta_pure_test_AtomicTest pureTest : pureSuite._tests()) + { + pureTest._testable(metamodel); + } + return pureSuite; + })); + } + + private static Root_meta_external_dataquality_DataQualityRelationValidationTestData buildRelationValidationTestData(org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestData testData, CompileContext context, ProcessingContext processingContext) + { + Root_meta_external_dataquality_DataQualityRelationValidationTestData pureTestData = new Root_meta_external_dataquality_DataQualityRelationValidationTestData_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationValidationTestData")); + if (testData.testData != null && !testData.testData.isEmpty()) + { + pureTestData._testData(ListIterate.collect(testData.testData, data -> buildStoreTestData(data, context, processingContext))); + } + return pureTestData; + } + + private static Root_meta_external_dataquality_DataQualityRelationComparisonTestData buildRelationComparisonTestData(org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestData testData, CompileContext context, ProcessingContext processingContext) + { + Root_meta_external_dataquality_DataQualityRelationComparisonTestData pureTestData = new Root_meta_external_dataquality_DataQualityRelationComparisonTestData_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationComparisonTestData")); + if (testData.testData != null && !testData.testData.isEmpty()) + { + pureTestData._testData(ListIterate.collect(testData.testData, data -> buildStoreTestData(data, context, processingContext))); + } + return pureTestData; + } + + private static Root_meta_external_dataquality_DataQualityStoreTestData buildStoreTestData(FunctionTestData testData, CompileContext context, ProcessingContext processingContext) + { + Root_meta_external_dataquality_DataQualityStoreTestData pureStoreData = new Root_meta_external_dataquality_DataQualityStoreTestData_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityStoreTestData")); + if (testData.doc != null) + { + pureStoreData._doc(testData.doc); + } + pureStoreData._element(context.pureModel.getPackageableElement(testData.packageableElementPointer.path, testData.packageableElementPointer.sourceInformation)); + pureStoreData._data(testData.data.accept(new EmbeddedDataFirstPassBuilder(context, processingContext))); + return pureStoreData; + } + + @Override + public List> getExtraTestProcessors() + { + return Collections.singletonList((test, context, processingContext) -> + { + if (test instanceof DataQualityRelationValidationTestSuite) + { + DataQualityRelationValidationTestSuite testSuite = (DataQualityRelationValidationTestSuite) test; + Root_meta_external_dataquality_DataQualityRelationValidationTestSuite pureSuite = new Root_meta_external_dataquality_DataQualityRelationValidationTestSuite_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationValidationTestSuite")); + pureSuite._id(testSuite.id); + if (testSuite.tests == null || testSuite.tests.isEmpty()) + { + throw new EngineException("DataQualityRelationValidation TestSuites should have at least 1 test", testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + List testIds = ListIterate.collect(testSuite.tests, t -> t.id); + List duplicateTestIds = testIds.stream().filter(e -> Collections.frequency(testIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateTestIds.isEmpty()) + { + throw new EngineException("Multiple tests found with ids : '" + String.join(",", duplicateTestIds) + "'", testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + RichIterable pureTests = ListIterate.collect(testSuite.tests, atomicTest -> (Root_meta_pure_test_AtomicTest) atomicTest.accept(new TestFirstPassBuilder(context, processingContext))); + pureSuite._tests(pureTests); + if (testSuite.testData != null) + { + pureSuite._testData(buildRelationValidationTestData(testSuite.testData, context, processingContext)); + } + return pureSuite; + } + else if (test instanceof DataQualityRelationValidationTest) + { + DataQualityRelationValidationTest atomicTest = (DataQualityRelationValidationTest) test; + Root_meta_external_dataquality_DataQualityRelationValidationTest_Impl pureTest = new Root_meta_external_dataquality_DataQualityRelationValidationTest_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationValidationTest")); + pureTest._id(atomicTest.id); + if (atomicTest.assertions == null || atomicTest.assertions.isEmpty()) + { + throw new EngineException("DataQualityRelationValidation Tests should have at least 1 assert", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + if (atomicTest.assertions.size() > 1) + { + throw new EngineException("DataQualityRelationValidation Tests support only 1 assertion per test", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + List assertionIds = ListIterate.collect(atomicTest.assertions, a -> a.id); + List duplicateAssertionIds = assertionIds.stream().filter(e -> Collections.frequency(assertionIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateAssertionIds.isEmpty()) + { + throw new EngineException("Multiple assertions found with ids : '" + String.join(",", duplicateAssertionIds) + "'", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + pureTest._assertions(ListIterate.collect(atomicTest.assertions, assertion -> assertion.accept(new TestAssertionFirstPassBuilder(context, processingContext)))); + return pureTest; + } + else if (test instanceof DataQualityRelationComparisonTestSuite) + { + DataQualityRelationComparisonTestSuite testSuite = (DataQualityRelationComparisonTestSuite) test; + Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite pureSuite = new Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationComparisonTestSuite")); + pureSuite._id(testSuite.id); + if (testSuite.tests == null || testSuite.tests.isEmpty()) + { + throw new EngineException("DataQualityRelationComparison TestSuites should have at least 1 test", testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + List testIds = ListIterate.collect(testSuite.tests, t -> t.id); + List duplicateTestIds = testIds.stream().filter(e -> Collections.frequency(testIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateTestIds.isEmpty()) + { + throw new EngineException("Multiple tests found with ids : '" + String.join(",", duplicateTestIds) + "'", testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + RichIterable pureTests = ListIterate.collect(testSuite.tests, atomicTest -> (Root_meta_pure_test_AtomicTest) atomicTest.accept(new TestFirstPassBuilder(context, processingContext))); + pureSuite._tests(pureTests); + if (testSuite.testData != null) + { + pureSuite._testData(buildRelationComparisonTestData(testSuite.testData, context, processingContext)); + } + return pureSuite; + } + else if (test instanceof DataQualityRelationComparisonTest) + { + DataQualityRelationComparisonTest atomicTest = (DataQualityRelationComparisonTest) test; + Root_meta_external_dataquality_DataQualityRelationComparisonTest_Impl pureTest = new Root_meta_external_dataquality_DataQualityRelationComparisonTest_Impl("", null, context.pureModel.getClass("meta::external::dataquality::DataQualityRelationComparisonTest")); + pureTest._id(atomicTest.id); + if (atomicTest.assertions == null || atomicTest.assertions.isEmpty()) + { + throw new EngineException("DataQualityRelationComparison Tests should have at least 1 assert", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + if (atomicTest.assertions.size() > 1) + { + throw new EngineException("DataQualityRelationComparison Tests support only 1 assertion per test", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + List assertionIds = ListIterate.collect(atomicTest.assertions, a -> a.id); + List duplicateAssertionIds = assertionIds.stream().filter(e -> Collections.frequency(assertionIds, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateAssertionIds.isEmpty()) + { + throw new EngineException("Multiple assertions found with ids : '" + String.join(",", duplicateAssertionIds) + "'", atomicTest.sourceInformation, EngineErrorType.COMPILATION); + } + pureTest._assertions(ListIterate.collect(atomicTest.assertions, assertion -> assertion.accept(new TestAssertionFirstPassBuilder(context, processingContext)))); + return pureTest; + } + return null; + }); + } + } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityLexerGrammar.g4 b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityLexerGrammar.g4 index 77fca9b5a27..3d5cd012a88 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityLexerGrammar.g4 +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityLexerGrammar.g4 @@ -45,3 +45,9 @@ RECON_EXPECTED_MATCH: 'expectedMatch'; // -------------------- PERSISTENCE STRATEGY ---------------------------------------------- PERSISTENCE_STRATEGY: 'persistenceStrategy'; + +// -------------------- TESTABLE ---------------------------------------------------------- +DQ_TEST_SUITES: 'testSuites'; +DQ_TEST_DATA: 'data'; +DQ_TEST_TESTS: 'tests'; +DQ_TEST_ASSERTS: 'asserts'; diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityParserGrammar.g4 b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityParserGrammar.g4 index 2bed7d8077c..608ded4c518 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityParserGrammar.g4 +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/DataQualityParserGrammar.g4 @@ -38,6 +38,10 @@ identifier: VALID_STRING | STRING | RECON_AGGREGATED_HASH | RECON_EXPECTED_MATCH | PERSISTENCE_STRATEGY + | DQ_TEST_SUITES + | DQ_TEST_DATA + | DQ_TEST_TESTS + | DQ_TEST_ASSERTS ; @@ -128,6 +132,7 @@ relationValidationDefinition: DATAQUALITYRELATIONVALIDATION stereotype relationFunc | validations | persistenceStrategy + | dqTestSuites )* BRACE_CLOSE ; @@ -170,6 +175,7 @@ relationComparisonDefinition: DATAQUALITYRELATIONCOMPARISON qualifiedName | columnsToCompare | reconStrategy | reconExpectedMatch + | dqTestSuites )* BRACE_CLOSE ; @@ -204,4 +210,45 @@ reconTargetHashColumn: RECON_TARGET_HASH_COLUMN COLON identifie reconAggregatedHash: RECON_AGGREGATED_HASH COLON BOOLEAN SEMI_COLON ; reconExpectedMatch: RECON_EXPECTED_MATCH COLON FLOAT SEMI_COLON +; + +// -------------------- Testable ---------------------------------------------------------- +dqTestSuites: DQ_TEST_SUITES COLON BRACKET_OPEN + (dqTestSuite (COMMA dqTestSuite)*)? + BRACKET_CLOSE +; +dqTestSuite: identifier COLON BRACE_OPEN + ( + dqTestSuiteData + | dqTestSuiteTests + )* + BRACE_CLOSE +; +dqTestSuiteData: DQ_TEST_DATA COLON BRACKET_OPEN + (dqStoreTestData (COMMA dqStoreTestData)*)? + BRACKET_CLOSE +; +dqStoreTestData: qualifiedName COLON dqEmbeddedData +; +dqEmbeddedData: identifier ISLAND_OPEN (dqEmbeddedDataContent)* +; +dqEmbeddedDataContent: ISLAND_START | ISLAND_BRACE_OPEN | ISLAND_CONTENT | ISLAND_HASH | ISLAND_BRACE_CLOSE | ISLAND_END +; +dqTestSuiteTests: DQ_TEST_TESTS COLON BRACKET_OPEN + (dqTestBlock (COMMA dqTestBlock)*)? + BRACKET_CLOSE +; +dqTestBlock: identifier COLON BRACE_OPEN + (dqTestAsserts)* + BRACE_CLOSE +; +dqTestAsserts: DQ_TEST_ASSERTS COLON BRACKET_OPEN + (dqTestAssert (COMMA dqTestAssert)*)? + BRACKET_CLOSE +; +dqTestAssert: identifier COLON dqTestAssertion +; +dqTestAssertion: identifier ISLAND_OPEN (dqTestAssertionContent)* +; +dqTestAssertionContent: ISLAND_START | ISLAND_BRACE_OPEN | ISLAND_CONTENT | ISLAND_HASH | ISLAND_BRACE_CLOSE | ISLAND_END ; \ No newline at end of file diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/from/DataQualityTreeWalker.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/from/DataQualityTreeWalker.java index d8462b62d59..41422fb5819 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/from/DataQualityTreeWalker.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/from/DataQualityTreeWalker.java @@ -22,7 +22,9 @@ import org.finos.legend.engine.language.pure.grammar.from.PureGrammarParserContext; import org.finos.legend.engine.language.pure.grammar.from.PureGrammarParserUtility; import org.finos.legend.engine.language.pure.grammar.from.antlr4.DataQualityParserGrammar; +import org.finos.legend.engine.language.pure.grammar.from.data.embedded.HelperEmbeddedDataGrammarParser; import org.finos.legend.engine.language.pure.grammar.from.domain.DomainParser; +import org.finos.legend.engine.language.pure.grammar.from.test.assertion.HelperTestAssertionGrammarParser; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQuality; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityExecutionContext; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityPropertyGraphFetchTree; @@ -34,6 +36,12 @@ import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityRelationComparison; import org.finos.legend.engine.protocol.dataquality.metamodel.MD5HashStrategy; import org.finos.legend.engine.protocol.dataquality.metamodel.ReconStrategy; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; import org.finos.legend.engine.protocol.pure.m3.SourceInformation; import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; import org.finos.legend.engine.protocol.pure.v1.model.context.PackageableElementPointer; @@ -42,9 +50,13 @@ import org.finos.legend.engine.protocol.pure.m3.extension.StereotypePtr; import org.finos.legend.engine.protocol.pure.m3.extension.TagPtr; import org.finos.legend.engine.protocol.pure.m3.extension.TaggedValue; +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.packageableElement.function.FunctionTestData; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.section.DefaultCodeSection; import org.finos.legend.engine.protocol.pure.m3.valuespecification.ValueSpecification; import org.finos.legend.engine.protocol.pure.m3.function.LambdaFunction; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.GraphFetchTree; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.PropertyGraphFetchTree; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.SubTypeGraphFetchTree; @@ -151,6 +163,14 @@ private DataQualityRelationComparison visitRelationComparison(DataQualityParserG relationComparison.expectedMatch = Double.parseDouble(reconExpectedMatchContext.FLOAT().getText()); } + // testSuites (optional) + DataQualityParserGrammar.DqTestSuitesContext testSuitesContext = PureGrammarParserUtility.validateAndExtractOptionalField( + ctx.dqTestSuites(), "testSuites", relationComparison.sourceInformation); + if (Objects.nonNull(testSuitesContext)) + { + relationComparison.testSuites = ListIterate.collect(testSuitesContext.dqTestSuite(), this::visitRelationComparisonTestSuite); + } + return relationComparison; } @@ -420,6 +440,14 @@ private DataqualityRelationValidation visitRelationValidation(DataQualityParserG dataqualityRelationValidation.persistenceStrategy = IDataQualityGrammarParserExtension.parsePersistenceStrategy(persistenceStrategyContext.islandDefinition(), this.walkerSourceInformation, this.parserContext.getPureGrammarParserExtensions()); } + // testSuites (optional) + DataQualityParserGrammar.DqTestSuitesContext testSuitesContext = PureGrammarParserUtility.validateAndExtractOptionalField( + relationValidationDefinitionContext.dqTestSuites(), "testSuites", dataqualityRelationValidation.sourceInformation); + if (Objects.nonNull(testSuitesContext)) + { + dataqualityRelationValidation.testSuites = ListIterate.collect(testSuitesContext.dqTestSuite(), this::visitRelationValidationTestSuite); + } + return dataqualityRelationValidation; } @@ -475,4 +503,93 @@ private String visitValidationType(DataQualityParserGrammar.ValidationTypeContex return validationTypeContext.validationTypeVal().VALIDATION_TYPE_AGG().getText(); } + // -------------------- Testable -------------------- + + private DataQualityRelationValidationTestSuite visitRelationValidationTestSuite(DataQualityParserGrammar.DqTestSuiteContext ctx) + { + DataQualityRelationValidationTestSuite suite = new DataQualityRelationValidationTestSuite(); + suite.sourceInformation = walkerSourceInformation.getSourceInformation(ctx); + suite.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + + DataQualityParserGrammar.DqTestSuiteDataContext dataContext = PureGrammarParserUtility.validateAndExtractOptionalField(ctx.dqTestSuiteData(), "data", suite.sourceInformation); + if (Objects.nonNull(dataContext)) + { + DataQualityRelationValidationTestData testData = new DataQualityRelationValidationTestData(); + testData.sourceInformation = walkerSourceInformation.getSourceInformation(dataContext); + testData.testData = visitStoreTestData(dataContext); + suite.testData = testData; + } + + DataQualityParserGrammar.DqTestSuiteTestsContext testsContext = PureGrammarParserUtility.validateAndExtractRequiredField(ctx.dqTestSuiteTests(), "tests", suite.sourceInformation); + suite.tests = ListIterate.collect(testsContext.dqTestBlock(), block -> visitAtomicTest(block, false)); + return suite; + } + + private DataQualityRelationComparisonTestSuite visitRelationComparisonTestSuite(DataQualityParserGrammar.DqTestSuiteContext ctx) + { + DataQualityRelationComparisonTestSuite suite = new DataQualityRelationComparisonTestSuite(); + suite.sourceInformation = walkerSourceInformation.getSourceInformation(ctx); + suite.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + + DataQualityParserGrammar.DqTestSuiteDataContext dataContext = PureGrammarParserUtility.validateAndExtractOptionalField(ctx.dqTestSuiteData(), "data", suite.sourceInformation); + if (Objects.nonNull(dataContext)) + { + DataQualityRelationComparisonTestData testData = new DataQualityRelationComparisonTestData(); + testData.sourceInformation = walkerSourceInformation.getSourceInformation(dataContext); + testData.testData = visitStoreTestData(dataContext); + suite.testData = testData; + } + + DataQualityParserGrammar.DqTestSuiteTestsContext testsContext = PureGrammarParserUtility.validateAndExtractRequiredField(ctx.dqTestSuiteTests(), "tests", suite.sourceInformation); + suite.tests = ListIterate.collect(testsContext.dqTestBlock(), block -> visitAtomicTest(block, true)); + return suite; + } + + private List visitStoreTestData(DataQualityParserGrammar.DqTestSuiteDataContext ctx) + { + List result = new ArrayList<>(); + if (ctx.dqStoreTestData() == null || ctx.dqStoreTestData().isEmpty()) + { + return result; + } + for (DataQualityParserGrammar.DqStoreTestDataContext storeCtx : ctx.dqStoreTestData()) + { + result.add(visitStoreTestDataEntry(storeCtx)); + } + return result; + } + + private FunctionTestData visitStoreTestDataEntry(DataQualityParserGrammar.DqStoreTestDataContext ctx) + { + FunctionTestData testData = new FunctionTestData(); + testData.sourceInformation = walkerSourceInformation.getSourceInformation(ctx); + String path = (ctx.qualifiedName().packagePath() == null ? "" : (PureGrammarParserUtility.fromPath(ctx.qualifiedName().packagePath().identifier()) + "::")) + + PureGrammarParserUtility.fromIdentifier(ctx.qualifiedName().identifier()); + testData.packageableElementPointer = new PackageableElementPointer( + PackageableElementType.STORE, + path, + walkerSourceInformation.getSourceInformation(ctx.qualifiedName()) + ); + testData.data = HelperEmbeddedDataGrammarParser.parseEmbeddedData(ctx.dqEmbeddedData(), this.walkerSourceInformation, this.parserContext.getPureGrammarParserExtensions()); + return testData; + } + + private org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest visitAtomicTest(DataQualityParserGrammar.DqTestBlockContext ctx, boolean forComparison) + { + org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest atomicTest = forComparison ? new DataQualityRelationComparisonTest() : new DataQualityRelationValidationTest(); + atomicTest.sourceInformation = walkerSourceInformation.getSourceInformation(ctx); + atomicTest.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + + DataQualityParserGrammar.DqTestAssertsContext assertsContext = PureGrammarParserUtility.validateAndExtractRequiredField(ctx.dqTestAsserts(), "asserts", atomicTest.sourceInformation); + atomicTest.assertions = ListIterate.collect(assertsContext.dqTestAssert(), this::visitTestAssertion); + return atomicTest; + } + + private TestAssertion visitTestAssertion(DataQualityParserGrammar.DqTestAssertContext ctx) + { + TestAssertion assertion = HelperTestAssertionGrammarParser.parseTestAssertion(ctx.dqTestAssertion(), this.walkerSourceInformation, this.parserContext.getPureGrammarParserExtensions()); + assertion.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + return assertion; + } + } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/to/DataQualityGrammarComposerExtension.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/to/DataQualityGrammarComposerExtension.java index 58066d23de1..928bb2fc41a 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/to/DataQualityGrammarComposerExtension.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/main/java/org/finos/legend/engine/language/dataquality/grammar/to/DataQualityGrammarComposerExtension.java @@ -24,7 +24,9 @@ import org.finos.legend.engine.language.pure.grammar.to.HelperValueSpecificationGrammarComposer; import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerContext; import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerUtility; +import org.finos.legend.engine.language.pure.grammar.to.data.HelperEmbeddedDataGrammarComposer; import org.finos.legend.engine.language.pure.grammar.to.extension.PureGrammarComposerExtension; +import org.finos.legend.engine.language.pure.grammar.to.test.assertion.HelperTestAssertionGrammarComposer; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQuality; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityPropertyGraphFetchTree; import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityRelationComparison; @@ -35,8 +37,15 @@ import org.finos.legend.engine.protocol.dataquality.metamodel.MappingAndRuntimeDataQualityExecutionContext; import org.finos.legend.engine.protocol.dataquality.metamodel.ReconStrategy; import org.finos.legend.engine.protocol.dataquality.metamodel.RelationValidation; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; import org.finos.legend.engine.protocol.pure.m3.PackageableElement; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.SubTypeGraphFetchTree; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; import org.finos.legend.engine.shared.core.api.grammar.RenderStyle; import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; @@ -98,6 +107,7 @@ private static String renderDataQualityRelationValidation(DataqualityRelationVal " query: " + renderRelationQuery(dataqualityRelationValidation, context) + " validations: " + renderValidations(dataqualityRelationValidation.validations, context) + renderPersistenceStrategy(dataqualityRelationValidation, context) + + renderRelationValidationTestSuites(dataqualityRelationValidation, context) + "}"; } @@ -124,6 +134,7 @@ private static String renderDataQualityRelationComparison(DataQualityRelationCom " strategy: " + renderReconStrategy(relationComparison.strategy) + ";\n" + (Objects.isNull(relationComparison.expectedMatch) ? "" : " expectedMatch: " + relationComparison.expectedMatch + ";\n") + + renderRelationComparisonTestSuites(relationComparison, context) + "}"; } @@ -360,4 +371,106 @@ private static String renderAssertion(RelationValidation relationValidation, Pur { return relationValidation.assertion.accept(DEPRECATED_PureGrammarComposerCore.Builder.newInstance(context).build()); } + + // -------------------- Testable rendering -------------------- + + private static String dqIndent(int level) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < level; i++) + { + sb.append(" "); + } + return sb.toString(); + } + + private static String renderRelationValidationTestSuites(DataqualityRelationValidation validation, PureGrammarComposerContext context) + { + if (Objects.isNull(validation.testSuites) || validation.testSuites.isEmpty()) + { + return ""; + } + String suites = validation.testSuites.stream() + .map(suite -> renderTestSuite(suite.id, suite.testData == null ? null : suite.testData.testData, suite.tests, 2, context)) + .collect(Collectors.joining(",\n")); + return dqIndent(1) + "testSuites:\n" + dqIndent(1) + "[\n" + suites + "\n" + dqIndent(1) + "];\n"; + } + + private static String renderRelationComparisonTestSuites(DataQualityRelationComparison comparison, PureGrammarComposerContext context) + { + if (Objects.isNull(comparison.testSuites) || comparison.testSuites.isEmpty()) + { + return ""; + } + String suites = comparison.testSuites.stream() + .map(suite -> renderTestSuite(suite.id, suite.testData == null ? null : suite.testData.testData, suite.tests, 2, context)) + .collect(Collectors.joining(",\n")); + return dqIndent(1) + "testSuites:\n" + dqIndent(1) + "[\n" + suites + "\n" + dqIndent(1) + "];\n"; + } + + private static String renderTestSuite(String suiteId, List testData, List tests, int baseIndent, PureGrammarComposerContext context) + { + StringBuilder str = new StringBuilder(); + str.append(dqIndent(baseIndent)).append(PureGrammarComposerUtility.convertIdentifier(suiteId)).append(":\n"); + str.append(dqIndent(baseIndent)).append("{\n"); + + if (testData != null && !testData.isEmpty()) + { + str.append(dqIndent(baseIndent + 1)).append("data:\n"); + str.append(dqIndent(baseIndent + 1)).append("[\n"); + str.append(testData.stream() + .map(td -> renderStoreTestData(td, baseIndent + 2, context)) + .collect(Collectors.joining(",\n"))); + str.append("\n").append(dqIndent(baseIndent + 1)).append("]\n"); + } + + if (tests != null && !tests.isEmpty()) + { + str.append(dqIndent(baseIndent + 1)).append("tests:\n"); + str.append(dqIndent(baseIndent + 1)).append("[\n"); + str.append(tests.stream() + .map(t -> renderAtomicTest(t, baseIndent + 2, context)) + .collect(Collectors.joining(",\n"))); + str.append("\n").append(dqIndent(baseIndent + 1)).append("]\n"); + } + + str.append(dqIndent(baseIndent)).append("}"); + return str.toString(); + } + + private static String renderStoreTestData(FunctionTestData testData, int baseIndent, PureGrammarComposerContext context) + { + StringBuilder str = new StringBuilder(); + str.append(dqIndent(baseIndent)).append(testData.packageableElementPointer.path).append(":\n"); + str.append(HelperEmbeddedDataGrammarComposer.composeEmbeddedData( + testData.data, + PureGrammarComposerContext.Builder.newInstance(context).withIndentationString(dqIndent(baseIndent + 1)).build())); + return str.toString(); + } + + private static String renderAtomicTest(AtomicTest test, int baseIndent, PureGrammarComposerContext context) + { + StringBuilder str = new StringBuilder(); + str.append(dqIndent(baseIndent)).append(PureGrammarComposerUtility.convertIdentifier(test.id)).append(":\n"); + str.append(dqIndent(baseIndent)).append("{\n"); + if (test.assertions != null && !test.assertions.isEmpty()) + { + str.append(dqIndent(baseIndent + 1)).append("asserts:\n"); + str.append(dqIndent(baseIndent + 1)).append("[\n"); + str.append(test.assertions.stream() + .map(a -> renderTestAssertion(a, baseIndent + 2, context)) + .collect(Collectors.joining(",\n"))); + str.append("\n").append(dqIndent(baseIndent + 1)).append("]\n"); + } + str.append(dqIndent(baseIndent)).append("}"); + return str.toString(); + } + + private static String renderTestAssertion(TestAssertion assertion, int baseIndent, PureGrammarComposerContext context) + { + String composed = HelperTestAssertionGrammarComposer.composeTestAssertion( + assertion, + PureGrammarComposerContext.Builder.newInstance(context).withIndentationString(dqIndent(baseIndent)).build()); + return composed; + } } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/test/java/org/finos/legend/engine/language/dataquality/grammar/to/TestDataQualityRoundtrip.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/test/java/org/finos/legend/engine/language/dataquality/grammar/to/TestDataQualityRoundtrip.java index 2b0fb0f5816..bde7ead0aac 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/test/java/org/finos/legend/engine/language/dataquality/grammar/to/TestDataQualityRoundtrip.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-grammar/src/test/java/org/finos/legend/engine/language/dataquality/grammar/to/TestDataQualityRoundtrip.java @@ -241,4 +241,91 @@ public void testRelationComparison_noKeysOrColumnsToCompare() "}\n"); } + @Test + public void testRelationValidation_withTestSuites() + { + test("###DataQualityValidation\n" + + "DataQualityRelationValidation meta::external::dataquality::testvalidation\n" + + "{\n" + + " query: |#>{my::Store.myTable}#->filter(c|$c.name == 'ok')->from(dq::dqDevRuntime);\n" + + " validations: [\n" + + " {\n" + + " name: 'testValidation';\n" + + " assertion: rel|$rel->assertRelationNotEmpty();\n" + + " }\n" + + " ];\n" + + " testSuites:\n" + + " [\n" + + " testSuite1:\n" + + " {\n" + + " data:\n" + + " [\n" + + " my::Store:\n" + + " Reference\n" + + " #{\n" + + " my::embedded::data\n" + + " }#\n" + + " ]\n" + + " tests:\n" + + " [\n" + + " test1:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assert1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected :\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ];\n" + + "}\n"); + } + + @Test + public void testRelationComparison_withTestSuites() + { + test("###DataQualityValidation\n" + + "DataQualityRelationComparison meta::external::dataquality::testRecon\n" + + "{\n" + + " source: src|#>{my::Store.sourceTable}#->filter(c|$c.active == true);\n" + + " target: tgt|#>{my::Store.targetTable}#->filter(c|$c.active == true);\n" + + " keys: [id];\n" + + " strategy: MD5Hash;\n" + + " testSuites:\n" + + " [\n" + + " testSuite1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " test1:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assert1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected :\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ];\n" + + "}\n"); + } + } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityProtocolExtension.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityProtocolExtension.java index ec9f8a2cb28..56974631d3a 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityProtocolExtension.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityProtocolExtension.java @@ -18,11 +18,17 @@ import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.Maps; import org.eclipse.collections.api.list.MutableList; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; import org.finos.legend.engine.protocol.pure.v1.extension.ProtocolSubTypeInfo; import org.finos.legend.engine.protocol.pure.v1.extension.PureProtocolExtension; import org.finos.legend.engine.protocol.pure.m3.PackageableElement; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.PropertyGraphFetchTree; import org.finos.legend.engine.protocol.pure.dsl.graph.valuespecification.constant.classInstance.RootGraphFetchTree; +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; +import org.finos.legend.engine.protocol.pure.v1.model.test.TestSuite; import java.util.List; import java.util.Map; @@ -57,6 +63,14 @@ public List>>> getExtraProtocolSubTypeInfo .withSubtype(DataQualityRelationComparison.class, "dataQualityRelationComparison") .build(), ProtocolSubTypeInfo.newBuilder(DataQualityPersistenceStrategy.class) + .build(), + ProtocolSubTypeInfo.newBuilder(TestSuite.class) + .withSubtype(DataQualityRelationValidationTestSuite.class, "dataQualityRelationValidationTestSuite") + .withSubtype(DataQualityRelationComparisonTestSuite.class, "dataQualityRelationComparisonTestSuite") + .build(), + ProtocolSubTypeInfo.newBuilder(AtomicTest.class) + .withSubtype(DataQualityRelationValidationTest.class, "dataQualityRelationValidationTest") + .withSubtype(DataQualityRelationComparisonTest.class, "dataQualityRelationComparisonTest") .build() )); } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityRelationComparison.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityRelationComparison.java index 706ecffaaba..55e09755553 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityRelationComparison.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataQualityRelationComparison.java @@ -15,6 +15,7 @@ package org.finos.legend.engine.protocol.dataquality.metamodel; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; import org.finos.legend.engine.protocol.pure.m3.extension.StereotypePtr; import org.finos.legend.engine.protocol.pure.m3.extension.TaggedValue; import org.finos.legend.engine.protocol.pure.m3.function.LambdaFunction; @@ -35,4 +36,5 @@ public class DataQualityRelationComparison extends ModelGenerationSpecification public List columnsToCompare = Collections.emptyList(); public ReconStrategy strategy; public Double expectedMatch; + public List testSuites = Collections.emptyList(); } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataqualityRelationValidation.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataqualityRelationValidation.java index 16c78830827..7c182285738 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataqualityRelationValidation.java +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/DataqualityRelationValidation.java @@ -15,6 +15,7 @@ package org.finos.legend.engine.protocol.dataquality.metamodel; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; import org.finos.legend.engine.protocol.pure.m3.extension.StereotypePtr; import org.finos.legend.engine.protocol.pure.m3.extension.TaggedValue; import org.finos.legend.engine.protocol.pure.m3.function.LambdaFunction; @@ -35,4 +36,5 @@ public class DataqualityRelationValidation extends ModelGenerationSpecification public LambdaFunction query; public List validations = Collections.emptyList(); public DataQualityPersistenceStrategy persistenceStrategy; + public List testSuites = Collections.emptyList(); } diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTest.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTest.java new file mode 100644 index 00000000000..6d2a2cb4116 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTest.java @@ -0,0 +1,21 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; + +public class DataQualityRelationComparisonTest extends AtomicTest +{ +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestData.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestData.java new file mode 100644 index 00000000000..1f9e3d6c6bd --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestData.java @@ -0,0 +1,27 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.m3.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; + +import java.util.Collections; +import java.util.List; + +public class DataQualityRelationComparisonTestData +{ + public List testData = Collections.emptyList(); + public SourceInformation sourceInformation; +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestSuite.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestSuite.java new file mode 100644 index 00000000000..c58dde77e28 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationComparisonTestSuite.java @@ -0,0 +1,22 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.v1.model.test.TestSuite; + +public class DataQualityRelationComparisonTestSuite extends TestSuite +{ + public DataQualityRelationComparisonTestData testData; +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTest.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTest.java new file mode 100644 index 00000000000..ae3cde8e0fc --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTest.java @@ -0,0 +1,21 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; + +public class DataQualityRelationValidationTest extends AtomicTest +{ +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestData.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestData.java new file mode 100644 index 00000000000..2fce8f1539f --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestData.java @@ -0,0 +1,27 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.m3.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; + +import java.util.Collections; +import java.util.List; + +public class DataQualityRelationValidationTestData +{ + public List testData = Collections.emptyList(); + public SourceInformation sourceInformation; +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestSuite.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestSuite.java new file mode 100644 index 00000000000..4a0edec4b18 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-protocol/src/main/java/org/finos/legend/engine/protocol/dataquality/metamodel/testable/DataQualityRelationValidationTestSuite.java @@ -0,0 +1,22 @@ +// Copyright 2026 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.protocol.dataquality.metamodel.testable; + +import org.finos.legend.engine.protocol.pure.v1.model.test.TestSuite; + +public class DataQualityRelationValidationTestSuite extends TestSuite +{ + public DataQualityRelationValidationTestData testData; +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure-test/src/main/resources/core_dataquality_test/lambda_relation_type_test.pure b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure-test/src/main/resources/core_dataquality_test/lambda_relation_type_test.pure new file mode 100644 index 00000000000..3b5e6c3776b --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure-test/src/main/resources/core_dataquality_test/lambda_relation_type_test.pure @@ -0,0 +1,163 @@ +// Copyright 2026 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::external::dataquality::*; +import meta::external::dataquality::datarecon::*; +import meta::external::dataquality::lambdaRelationType::*; +import meta::external::dataquality::tests::*; +import meta::external::query::sql::transformation::compile::utils::*; +import meta::pure::metamodel::relation::*; + +// Test compiler for `getLambdaRelationType`; mirrors the compiler used by the DQ recon / +// sample-values tests (compileViaGrammar), which is what the engine wires in at runtime. +function meta::external::dataquality::tests::lambdaRelationType::testCompiler(): Function<{LambdaFunction[1]->LambdaFunction[1]}>[1] +{ + {l:LambdaFunction[1] | compileViaGrammar($l)} +} + +function meta::external::dataquality::tests::lambdaRelationType::loadDataQualityRelationComparison(dq:String[1]): DataQualityRelationComparison[1] +{ + loadModel($dq) + ->filter(c | $c->elementToPath() == 'meta::external::dataquality::tests::domain::RelationComparison') + ->toOne()->cast(@DataQualityRelationComparison); +} + +// -------------------- Validation branch -------------------- +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationValidation_assertRelationNotEmpty():Boolean[1] +{ + let element = loadDataQualityRelationValidation( + '###DataQualityValidation' + + ' DataQualityRelationValidation meta::external::dataquality::tests::domain::RelationValidation' + + ' {' + + ' query: #>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[FIRSTNAME, LASTNAME]);' + + ' validations: [' + + ' {' + + ' name: \'nonEmptyDataset\';' + + ' description: \'dataset should not be empty\';' + + ' assertion: rel|$rel->meta::external::dataquality::assertRelationNotEmpty();' + + ' }' + + ' ];' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + // assertRelationNotEmpty → buildOneRowIfEmptyExpression → COUNT-only relation. + assertEquals(['COUNT'], $relType.columns.name); +} + +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationValidation_assertRelationEmpty():Boolean[1] +{ + let element = loadDataQualityRelationValidation( + '###DataQualityValidation' + + ' DataQualityRelationValidation meta::external::dataquality::tests::domain::RelationValidation' + + ' {' + + ' query: #>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, FIRSTNAME, LASTNAME]);' + + ' validations: [' + + ' {' + + ' name: \'nameNotEmpty\';' + + ' description: \'name columns should not be empty\';' + + ' assertion: rel|$rel->meta::external::dataquality::assertRelationEmpty(~[FIRSTNAME, LASTNAME]);' + + ' }' + + ' ];' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + // assertRelationEmpty(~[cols]) → buildSelectExpression → projected columns only. + assertEquals(['FIRSTNAME', 'LASTNAME'], $relType.columns.name); +} + +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationValidation_assertRelationEmptyWithFilter():Boolean[1] +{ + let element = loadDataQualityRelationValidation( + '###DataQualityValidation' + + ' DataQualityRelationValidation meta::external::dataquality::tests::domain::RelationValidation' + + ' {' + + ' query: #>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, FIRSTNAME, LASTNAME]);' + + ' validations: [' + + ' {' + + ' name: \'invalidFirstName\';' + + ' description: \'first name should not be \\\'invalid\\\'\';' + + ' assertion: rel|$rel->filter(row|$row.FIRSTNAME == \'invalid\')->meta::external::dataquality::assertRelationEmpty(~[FIRSTNAME]);' + + ' }' + + ' ];' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + // Filter is preserved upstream; projection controls the emitted column set. + assertEquals(['FIRSTNAME'], $relType.columns.name); +} + +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationValidation_queryEndsWithFrom():Boolean[1] +{ + let element = loadDataQualityRelationValidation( + '###DataQualityValidation' + + ' DataQualityRelationValidation meta::external::dataquality::tests::domain::RelationValidation' + + ' {' + + ' query: #>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, FIRSTNAME, LASTNAME])' + + ' ->from(meta::external::dataquality::tests::domain::dataqualitymappings, meta::external::dataquality::tests::domain::DataQualityRuntime);' + + ' validations: [' + + ' {' + + ' name: \'nameNotEmpty\';' + + ' description: \'name columns should not be empty\';' + + ' assertion: rel|$rel->meta::external::dataquality::assertRelationEmpty(~[FIRSTNAME, LASTNAME]);' + + ' }' + + ' ];' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + assertEquals(['FIRSTNAME', 'LASTNAME'], $relType.columns.name); +} + +// -------------------- Comparison branch -------------------- + +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationComparison_dynamicHash():Boolean[1] +{ + let element = meta::external::dataquality::tests::lambdaRelationType::loadDataQualityRelationComparison( + '###DataQualityValidation' + + ' DataQualityRelationComparison meta::external::dataquality::tests::domain::RelationComparison' + + ' {' + + ' source: |#>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, FIRSTNAME, LASTNAME]);' + + ' target: |#>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, FIRSTNAME, LASTNAME]);' + + ' keys: [ID];' + + ' columnsToCompare: [FIRSTNAME, LASTNAME];' + + ' strategy: MD5Hash;' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + // Reconciliation output columns: hash cols first, then key SOURCE/TARGET, then compare SOURCE/TARGET. + assertEquals( + ['DIGEST_SOURCE', 'DIGEST_TARGET', 'ID_SOURCE', 'ID_TARGET', 'FIRSTNAME_SOURCE', 'FIRSTNAME_TARGET', 'LASTNAME_SOURCE', 'LASTNAME_TARGET'], + $relType.columns.name + ); +} + +function <> meta::external::dataquality::tests::lambdaRelationType::testGetLambdaRelationType_relationComparison_precomputedHash():Boolean[1] +{ + let element = meta::external::dataquality::tests::lambdaRelationType::loadDataQualityRelationComparison( + '###DataQualityValidation' + + ' DataQualityRelationComparison meta::external::dataquality::tests::domain::RelationComparison' + + ' {' + + ' source: |#>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, HASH]);' + + ' target: |#>{meta::external::dataquality::tests::domain::db.personTable}#->select(~[ID, HASH]);' + + ' keys: [ID];' + + ' strategy: MD5Hash' + + ' {' + + ' sourceHashColumn: HASH;' + + ' targetHashColumn: HASH;' + + ' aggregatedHash: false;' + + ' };' + + ' }' + ); + let relType = getLambdaRelationType($element, meta::external::dataquality::tests::lambdaRelationType::testCompiler()); + assertEquals(['HASH_SOURCE', 'HASH_TARGET', 'ID_SOURCE', 'ID_TARGET'], $relType.columns.name); +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/datarecon.pure b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/datarecon.pure index 7237017a5d2..4284d8204fe 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/datarecon.pure +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/datarecon.pure @@ -81,11 +81,7 @@ function meta::external::dataquality::datarecon::getDataReconLambda(reconInput: $withDqCols->meta::external::dataquality::buildOrderedSelectExpression($orderedCols, $projectedRelType); ); - // Build final lambda - if there are parameters, create new lambda with merged params; otherwise preserve baseLambda structure - if ($allParams->isEmpty(), - | ^$baseLambda(expressionSequence=$withOrderedCols), - | lambda(^FunctionType(returnMultiplicity=$withOrderedCols.multiplicity, returnType=$withOrderedCols.genericType, parameters=$allParams), $withOrderedCols) - ); + lambda(^FunctionType(returnMultiplicity=$withOrderedCols.multiplicity, returnType=$withOrderedCols.genericType, parameters=$allParams), $withOrderedCols); } function meta::external::dataquality::datarecon::buildFinalColumnOrder(currentColNames: String[*], keys: String[*], compareCols: String[*], sourceHashCol: String[1], targetHashCol: String[1]): String[*] diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/lambda_relation_type.pure b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/lambda_relation_type.pure new file mode 100644 index 00000000000..ee2ae714a24 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/generation/lambda_relation_type.pure @@ -0,0 +1,65 @@ +// Copyright 2026 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::metamodel::relation::*; +import meta::external::dataquality::*; +import meta::external::dataquality::datarecon::*; + +function meta::external::dataquality::lambdaRelationType::getLambdaRelationType( + element: PackageableElement[1], + compiler: Function<{LambdaFunction[1]->LambdaFunction[1]}>[1] +): RelationType[1] +{ + let lambda = $element->match([ + v: DataQualityRelationValidation[1] | generateDataqualityRelationValidationLambda($v, [], [], false, false, true, false), + c: DataQualityRelationComparison[1] | let reconInput = createReconInput( + $c.source, + $c.target, + $c.keys, + meta::external::dataquality::lambdaRelationType::aggregatedHashOf($c.strategy), + $c.columnsToCompare, + meta::external::dataquality::lambdaRelationType::sourceHashColumnOf($c.strategy), + meta::external::dataquality::lambdaRelationType::targetHashColumnOf($c.strategy), + true + ); + getDataReconLambda($reconInput, $compiler);, + e: PackageableElement[1] | fail('Unsupported DataQuality element for relation type: ' + $e->elementToPath())->cast(@LambdaFunction); + ]); + $lambda.expressionSequence->last()->evaluateAndDeactivate().genericType.typeArguments.rawType->toOne()->cast(@RelationType); +} + +// Strategy field extractors — mirror the Java helpers previously in DataQualityExecute. +function <> meta::external::dataquality::lambdaRelationType::aggregatedHashOf(strategy: ReconStrategy[1]): Boolean[1] +{ + $strategy->match([ + m: MD5HashStrategy[1] | $m.aggregatedHash, + s: ReconStrategy[1] | false + ]); +} + +function <> meta::external::dataquality::lambdaRelationType::sourceHashColumnOf(strategy: ReconStrategy[1]): String[0..1] +{ + $strategy->match([ + m: MD5HashStrategy[1] | $m.sourceHashColumn, + s: ReconStrategy[1] | [] + ]); +} + +function <> meta::external::dataquality::lambdaRelationType::targetHashColumnOf(strategy: ReconStrategy[1]): String[0..1] +{ + $strategy->match([ + m: MD5HashStrategy[1] | $m.targetHashColumn, + s: ReconStrategy[1] | [] + ]); +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/metamodel/metamodel.pure b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/metamodel/metamodel.pure index 1866df537de..7268618a30b 100644 --- a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/metamodel/metamodel.pure +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-pure/src/main/resources/core_dataquality/metamodel/metamodel.pure @@ -49,7 +49,7 @@ Class meta::external::dataquality::DataQualityRule propertyPath: String[1]; } -Class meta::external::dataquality::DataQualityRelationValidation extends PackageableElement +Class meta::external::dataquality::DataQualityRelationValidation extends PackageableElement, meta::pure::metamodel::testable::Testable { query: LambdaFunction[1]; // should return a relation - enforced in compiler validations: RelationValidation[*]; @@ -87,7 +87,7 @@ Class meta::external::dataquality::datarecon::DataQualityReconInput enrichDQColumns: Boolean[1] = false; //whether to add DQ columns to the response } -Class meta::external::dataquality::DataQualityRelationComparison extends PackageableElement +Class meta::external::dataquality::DataQualityRelationComparison extends PackageableElement, meta::pure::metamodel::testable::Testable [ sourceMustReturnRelation: $this.source->functionReturnType().rawType->toOne()->subTypeOf(Relation), targetMustReturnRelation: $this.target->functionReturnType().rawType->toOne()->subTypeOf(Relation) @@ -110,4 +110,41 @@ Class meta::external::dataquality::MD5HashStrategy extends meta::external::dataq sourceHashColumn: String[0..1]; targetHashColumn: String[0..1]; aggregatedHash: Boolean[1]; +} + +// -------------------- Testable model -------------------- + +Class meta::external::dataquality::DataQualityRelationValidationTestSuite extends meta::pure::test::TestSuite +{ + testData: meta::external::dataquality::DataQualityRelationValidationTestData[0..1]; +} + +Class meta::external::dataquality::DataQualityRelationValidationTest extends meta::pure::test::AtomicTest +{ +} + +Class meta::external::dataquality::DataQualityRelationValidationTestData +{ + testData: meta::external::dataquality::DataQualityStoreTestData[*]; +} + +Class meta::external::dataquality::DataQualityRelationComparisonTestSuite extends meta::pure::test::TestSuite +{ + testData: meta::external::dataquality::DataQualityRelationComparisonTestData[0..1]; +} + +Class meta::external::dataquality::DataQualityRelationComparisonTest extends meta::pure::test::AtomicTest +{ +} + +Class meta::external::dataquality::DataQualityRelationComparisonTestData +{ + testData: meta::external::dataquality::DataQualityStoreTestData[*]; +} + +Class meta::external::dataquality::DataQualityStoreTestData +{ + doc: String[0..1]; + element: meta::pure::metamodel::PackageableElement[1]; + data: meta::pure::data::EmbeddedData[1]; } \ No newline at end of file diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/pom.xml b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/pom.xml new file mode 100644 index 00000000000..c7583501c2f --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/pom.xml @@ -0,0 +1,158 @@ + + + + legend-engine-xts-dataquality + org.finos.legend.engine + dq-test-suite + + 4.0.0 + + legend-engine-xt-dataquality-test-runner + Legend Engine - XT - DataQuality - Test Runner + + + + + org.finos.legend.pure + legend-pure-m3-core + + + + + + org.finos.legend.engine + legend-engine-pure-code-compiled-core + + + org.finos.legend.engine + legend-engine-pure-code-core-extension + + + org.finos.legend.engine + legend-engine-testable + + + org.finos.legend.engine + legend-engine-shared-core + + + org.finos.legend.engine + legend-engine-protocol-pure + + + org.finos.legend.engine + legend-engine-language-pure-compiler + + + org.finos.legend.engine + legend-engine-language-pure-grammar + runtime + + + org.finos.legend.engine + legend-engine-executionPlan-generation + + + org.bouncycastle + * + + + + + org.finos.legend.engine + legend-engine-executionPlan-execution + + + + + + org.finos.legend.engine + legend-engine-xt-dataquality-pure + + + org.finos.legend.engine + legend-engine-xt-dataquality-protocol + + + org.finos.legend.engine + legend-engine-xt-dataquality-compiler + runtime + + + org.finos.legend.engine + legend-engine-xt-dataquality-generation + + + + + + org.finos.legend.engine + legend-engine-pure-platform-dsl-store-java + + + org.finos.legend.pure + legend-pure-m2-dsl-store-pure + + + + + + org.eclipse.collections + eclipse-collections-api + + + org.eclipse.collections + eclipse-collections + + + + + + junit + junit + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-grammar + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-protocol + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-duckdb-grammar + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-duckdb-execution + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-executionPlan + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-javaPlatformBinding-pure + test + + + org.finos.legend.engine + legend-engine-configuration-contract-extension-pure + test + + + org.finos.legend.engine + legend-engine-configuration-plan-generation-serialization + test + + + + + diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestRunner.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestRunner.java new file mode 100644 index 00000000000..73f36676b97 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestRunner.java @@ -0,0 +1,401 @@ +// Copyright 2026 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.testable.dataquality.extension; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.factory.Sets; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.map.MutableMap; +import org.eclipse.collections.api.tuple.Pair; +import org.eclipse.collections.impl.tuple.Tuples; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.generation.dataquality.DataQualityReconLambdaGenerator; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.ConnectionFirstPassBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.HelperModelBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.data.core.EmbeddedDataCompilerHelper; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.StoreProviderCompilerHelper; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.RelationAccessorTestConnectionFactory; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.execution.result.Result; +import org.finos.legend.engine.plan.execution.result.serialization.SerializationFormat; +import org.finos.legend.engine.plan.generation.PlanGenerator; +import org.finos.legend.engine.plan.generation.extension.PlanGeneratorExtension; +import org.finos.legend.engine.plan.platform.PlanPlatform; +import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityRelationComparison; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationComparisonTestSuite; +import org.finos.legend.engine.protocol.pure.v1.extension.ConnectionFactoryExtension; +import org.finos.legend.engine.protocol.pure.v1.extension.TestConnectionBuildParameters; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.data.DataElementReference; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedData; +import org.finos.legend.engine.protocol.pure.v1.model.data.relation.RelationElementsData; +import org.finos.legend.engine.protocol.pure.v1.model.executionPlan.SingleExecutionPlan; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.Connection; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.Store; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.AssertionStatus; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestExecuted; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestResult; +import org.finos.legend.engine.pure.code.core.PureCoreExtensionLoader; +import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; +import org.finos.legend.engine.testable.assertion.TestAssertionEvaluator; +import org.finos.legend.engine.testable.extension.AbstractTestSuiteSessionWithResources; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.extension.TestSuiteSession; +import org.finos.legend.engine.testable.helper.TestResultHelper; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Connection; +import org.finos.legend.pure.generated.Root_meta_core_runtime_ConnectionStore; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparison; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_MD5HashStrategy; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_ReconStrategy; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_datarecon_DataQualityReconInput; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; +import org.finos.legend.pure.generated.Root_meta_pure_test_TestSuite; +import org.finos.legend.pure.generated.core_dataquality_generation_datarecon; +import org.finos.legend.pure.generated.core_pure_corefunctions_metaExtension; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.PackageableElement; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.FunctionDefinition; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.LambdaFunction; + +import java.io.Closeable; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Consumer; + +public class DataQualityRelationComparisonTestRunner implements TestRunner +{ + private final Root_meta_external_dataquality_DataQualityRelationComparison pureElement; + private final MutableList extensions; + private final MutableList connectionBuilders = + Lists.mutable.withAll(ServiceLoader.load(ConnectionFactoryExtension.class)); + private final MutableList connectionAndDatabaseBuilders = + Lists.mutable.withAll(ServiceLoader.load(RelationAccessorTestConnectionFactory.class)); + private final PlanExecutor executor; + private final String pureVersion; + + public DataQualityRelationComparisonTestRunner(Root_meta_external_dataquality_DataQualityRelationComparison pureElement, String pureVersion) + { + this.pureElement = pureElement; + this.pureVersion = pureVersion; + this.executor = PlanExecutor.newPlanExecutorBuilder().withAvailableStoreExecutors().build(); + this.extensions = Lists.mutable.withAll(ServiceLoader.load(PlanGeneratorExtension.class)); + } + + @Override + public TestResult executeAtomicTest(Root_meta_pure_test_AtomicTest atomicTest, PureModel pureModel, PureModelContextData data) + { + throw new UnsupportedOperationException("DataQualityRelationComparison Test should be executed in the context of a Test Suite only"); + } + + @Override + public TestSuiteSession openTestSuiteSession(Root_meta_pure_test_TestSuite testSuite, PureModel pureModel, PureModelContextData data) + { + if (!(testSuite instanceof Root_meta_external_dataquality_DataQualityRelationComparisonTestSuite)) + { + throw new EngineException("Expected DataQualityRelationComparisonTestSuite. Found: " + testSuite.getClass().getSimpleName()); + } + String testablePath = HelperModelBuilder.getElementFullPath(this.pureElement, pureModel.getExecutionSupport()); + DataQualityRelationComparison protocolElement = ListIterate.detect(data.getElementsOfType(DataQualityRelationComparison.class), e -> e.getPath().equals(testablePath)); + if (protocolElement == null) + { + throw new EngineException("Cannot find DataQualityRelationComparison '" + testablePath + "' in the model context"); + } + DataQualityRelationComparisonTestSuite protocolSuite = ListIterate.detect(protocolElement.testSuites, s -> s.id.equals(testSuite._id())); + return new DataQualityRelationComparisonTestSuiteSession(testSuite, protocolSuite, pureModel, data, testablePath); + } + + private class DataQualityRelationComparisonTestSuiteSession + extends AbstractTestSuiteSessionWithResources + { + private final String testablePath; + private LambdaFunction effectiveReconLambda; + + DataQualityRelationComparisonTestSuiteSession( + Root_meta_pure_test_TestSuite pureSuite, + DataQualityRelationComparisonTestSuite protocolSuite, + PureModel pureModel, + PureModelContextData pmcd, + String testablePath) + { + super(pureSuite, protocolSuite, pureModel, pmcd, + DataQualityRelationComparisonTestSuiteSession::getTestSuiteTests, + DataQualityRelationComparisonTestSuiteSession::getAtomicTestId); + this.testablePath = testablePath; + } + + @Override + @SuppressWarnings("unchecked") + protected void initialize(Consumer closeableConsumer) + { + LambdaFunction reconLambda = generateReconLambda( + (LambdaFunction) DataQualityRelationComparisonTestRunner.this.pureElement._source(), + (LambdaFunction) DataQualityRelationComparisonTestRunner.this.pureElement._target()); + + DataQualityRelationComparisonTestData testData = this.protocolSuite.testData; + if (testData == null || testData.testData == null || testData.testData.isEmpty()) + { + this.effectiveReconLambda = reconLambda; + return; + } + MutableList storeTestData = Lists.mutable.empty(); + MutableList nonStoreRelationTestData = Lists.mutable.empty(); + for (FunctionTestData td : testData.testData) + { + try + { + StoreProviderCompilerHelper.getStoreFromPackageableElementPointer( + td.packageableElementPointer, this.pureModel.getContext()); + storeTestData.add(td); + } + catch (Exception e) + { + nonStoreRelationTestData.add(td); + } + } + if (storeTestData.notEmpty() && nonStoreRelationTestData.notEmpty()) + { + throw new EngineException("Error in DataQualityRelationComparison test suite " + this.protocolSuite.id + + ". The combination of store and non-store relation test data is not supported"); + } + if (storeTestData.notEmpty()) + { + mockConnectionsOnReconLambda(reconLambda, storeTestData); + this.effectiveReconLambda = reconLambda; + } + else + { + MutableMap relationData = + buildRelationTestData(nonStoreRelationTestData, this.pureModel, this.pmcd); + this.effectiveReconLambda = relationData.isEmpty() + ? reconLambda + : (LambdaFunction) rewriteLambda((FunctionDefinition) reconLambda, relationData, this.pureModel); + } + } + + private void mockConnectionsOnReconLambda(LambdaFunction reconLambda, List storeTestData) + { + RichIterable runtimes = + core_pure_corefunctions_metaExtension.Root_meta_pure_functions_meta_extractRuntimesFromFunctionDefinition_FunctionDefinition_1__Runtime_MANY_( + reconLambda, this.pureModel.getExecutionSupport()); + if (runtimes.isEmpty()) + { + // Neither side uses ->from(runtime) so there is nothing to mock; the plan will run + // against the raw stores as-is. + return; + } + MutableMap> connectionMap = Maps.mutable.empty(); + runtimes.forEach(runtime -> + runtime._connectionStores().forEach(cs -> + connectionMap.getIfAbsentPut(cs._connection(), Lists.mutable::empty).add(cs))); + List> originalConnections = Lists.mutable.empty(); + ConnectionFirstPassBuilder connectionVisitor = new ConnectionFirstPassBuilder(this.pureModel.getContext()); + connectionMap.forEachKeyValue((connection, connectionStores) -> + { + MutableMap storeEmbeddedDataMap = Maps.mutable.empty(); + connectionStores.forEach(connectionStore -> + { + Object element = connectionStore._element(); + if (!(element instanceof org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store)) + { + return; + } + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store metamodelStore = + (org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store) element; + String connectionStorePath = HelperModelBuilder.getElementFullPath( + metamodelStore, this.pureModel.getExecutionSupport()); + Optional matchingTestData = storeTestData.stream().filter(td -> + { + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store tdStore = + StoreProviderCompilerHelper.getStoreFromPackageableElementPointer( + td.packageableElementPointer, this.pureModel.getContext()); + return connectionStorePath.equals(HelperModelBuilder.getElementFullPath( + tdStore, this.pureModel.getExecutionSupport())); + }).findFirst(); + if (matchingTestData.isPresent()) + { + FunctionTestData resolved = matchingTestData.get(); + EmbeddedData data = resolved.data instanceof DataElementReference + ? EmbeddedDataCompilerHelper.getEmbeddedDataFromDataElement( + (DataElementReference) resolved.data, this.pmcd) + : resolved.data; + Store protocolStore = resolveProtocolStore(this.pmcd, connectionStorePath); + if (protocolStore != null) + { + storeEmbeddedDataMap.put(protocolStore, data); + } + } + }); + if (storeEmbeddedDataMap.isEmpty()) + { + return; + } + Pair> mockConnection = buildTestConnection(storeEmbeddedDataMap); + Root_meta_core_runtime_Connection compiledMock = mockConnection.getOne().accept(connectionVisitor); + connectionStores.forEach(cs -> + { + originalConnections.add(Tuples.pair(cs, cs._connection())); + cs._connection(compiledMock); + }); + if (mockConnection.getTwo() != null) + { + registerCloseables(mockConnection.getTwo()); + } + }); + registerCloseable(() -> originalConnections.forEach(p -> p.getOne()._connection(p.getTwo()))); + } + + private Pair> buildTestConnection(MutableMap storeEmbeddedDataMap) + { + TestConnectionBuildParameters hints = TestConnectionBuildParameters.newBuilder().withIsRelation(true).build(); + for (ConnectionFactoryExtension factory : DataQualityRelationComparisonTestRunner.this.connectionBuilders) + { + Optional>> optional = factory.tryBuildConnectionForStoreData( + Collections.emptyMap(), storeEmbeddedDataMap, hints); + if (optional != null && optional.isPresent()) + { + return optional.get(); + } + } + throw new EngineException("No ConnectionFactoryExtension could build a test connection for the " + + "DataQualityRelationComparison test suite " + this.protocolSuite.id); + } + + private Store resolveProtocolStore(PureModelContextData pmcd, String path) + { + return ListIterate.detect(pmcd.getElementsOfType(Store.class), s -> s.getPath().equals(path)); + } + + @Override + protected TestResult buildErrorResult(String atomicTestId, Throwable t) + { + return TestResultHelper.newTestError(this.testablePath, getTestSuiteId(), atomicTestId, t); + } + + @Override + protected TestResult runAtomicTest(DataQualityRelationComparisonTest atomicTest) + { + TestResult testResult = executeComparisonTest(atomicTest); + testResult.testable = this.testablePath; + testResult.testSuiteId = getTestSuiteId(); + return testResult; + } + + private TestResult executeComparisonTest(DataQualityRelationComparisonTest atomicTest) + { + try + { + SingleExecutionPlan plan = PlanGenerator.generateExecutionPlan( + this.effectiveReconLambda, + null, + null, + null, + this.pureModel, + DataQualityRelationComparisonTestRunner.this.pureVersion, + PlanPlatform.JAVA, + null, + PureCoreExtensionLoader.extensions().flatCollect(e -> e.extraPureCoreExtensions(this.pureModel.getExecutionSupport())), + DataQualityRelationComparisonTestRunner.this.extensions.flatCollect(PlanGeneratorExtension::getExtraPlanTransformers)); + + Result result = DataQualityRelationComparisonTestRunner.this.executor.execute(plan); + // DataQualityRelationComparison tests are constrained (at compile time) to a + // single assertion per atomic test. See DataQualityCompilerExtension for the + // enforcement. + TestAssertion assertion = atomicTest.assertions.get(0); + AssertionStatus assertionStatus = assertion.accept(new TestAssertionEvaluator(result, SerializationFormat.RAW)); + TestExecuted testResult = new TestExecuted(Collections.singletonList(assertionStatus)); + testResult.atomicTestId = atomicTest.id; + return testResult; + } + catch (Exception e) + { + return TestResultHelper.newTestError(atomicTest.id, e); + } + } + + private LambdaFunction generateReconLambda(LambdaFunction source, LambdaFunction target) + { + Root_meta_external_dataquality_DataQualityRelationComparison element = DataQualityRelationComparisonTestRunner.this.pureElement; + Root_meta_external_dataquality_ReconStrategy strategy = element._strategy(); + boolean aggregatedHash = strategy instanceof Root_meta_external_dataquality_MD5HashStrategy && ((Root_meta_external_dataquality_MD5HashStrategy) strategy)._aggregatedHash(); + String sourceHash = strategy instanceof Root_meta_external_dataquality_MD5HashStrategy ? ((Root_meta_external_dataquality_MD5HashStrategy) strategy)._sourceHashColumn() : null; + String targetHash = strategy instanceof Root_meta_external_dataquality_MD5HashStrategy ? ((Root_meta_external_dataquality_MD5HashStrategy) strategy)._targetHashColumn() : null; + + Root_meta_external_dataquality_datarecon_DataQualityReconInput reconInput = + core_dataquality_generation_datarecon.Root_meta_external_dataquality_datarecon_createReconInput_LambdaFunction_1__LambdaFunction_1__String_MANY__Boolean_1__String_MANY__String_$0_1$__String_$0_1$__Boolean_1__Integer_$0_1$__Boolean_1__Boolean_1__DataQualityReconInput_1_( + source, + target, + element._keys(), + aggregatedHash, + element._columnsToCompare(), + sourceHash, + targetHash, + true, + null, + false, + false, + this.pureModel.getExecutionSupport() + ); + return DataQualityReconLambdaGenerator.generateLambda(this.pureModel, reconInput); + } + } + + private MutableMap buildRelationTestData(List testData, PureModel pureModel, PureModelContextData pmcd) + { + MutableMap relationData = Maps.mutable.empty(); + for (FunctionTestData td : testData) + { + EmbeddedData data = td.data instanceof DataElementReference + ? EmbeddedDataCompilerHelper.getEmbeddedDataFromDataElement((DataElementReference) td.data, pmcd) + : td.data; + if (data instanceof RelationElementsData) + { + PackageableElement element = pureModel.getPackageableElement(td.packageableElementPointer.path); + relationData.put(element, (RelationElementsData) data); + } + } + return relationData; + } + + private FunctionDefinition rewriteLambda(FunctionDefinition lambda, MutableMap relationData, PureModel pureModel) + { + if (relationData.isEmpty()) + { + return lambda; + } + MutableList> rewritten = this.connectionAndDatabaseBuilders + .collect(f -> f.rewriteFunctionForTestDataExecution(lambda, relationData, pureModel)); + rewritten.removeIf(Objects::isNull); + if (rewritten.isEmpty()) + { + throw new EngineException("No RelationAccessorTestConnectionFactory could handle the test data for the DataQualityRelationComparison lambda"); + } + if (rewritten.size() > 1) + { + throw new EngineException("Multiple RelationAccessorTestConnectionFactory implementations rewrote the DataQualityRelationComparison lambda; expected exactly one match"); + } + return rewritten.get(0); + } +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestableRunnerExtension.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestableRunnerExtension.java new file mode 100644 index 00000000000..3c11e8d7704 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationComparisonTestableRunnerExtension.java @@ -0,0 +1,71 @@ +// Copyright 2026 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.testable.dataquality.extension; + +import org.eclipse.collections.api.list.MutableList; +import org.finos.legend.engine.protocol.dataquality.metamodel.DataQualityRelationComparison; +import org.finos.legend.engine.protocol.pure.PureClientVersions; +import org.finos.legend.engine.protocol.pure.m3.PackageableElement; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.extension.TestableRunnerExtension; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationComparison; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.testable.Testable; + +public class DataQualityRelationComparisonTestableRunnerExtension implements TestableRunnerExtension +{ + public static final String CLASSIFIER_PATH = "meta::external::dataquality::DataQualityRelationComparison"; + + private String pureVersion = PureClientVersions.production; + + @Override + public MutableList group() + { + return org.eclipse.collections.impl.factory.Lists.mutable.with("PackageableElement", "DataQualityValidation"); + } + + @Override + public String getSupportedClassifierPath() + { + return CLASSIFIER_PATH; + } + + @Override + public Boolean isTestable(PackageableElement element) + { + return element instanceof DataQualityRelationComparison; + } + + @Override + public Boolean isTestableEmpty(PackageableElement element) + { + DataQualityRelationComparison comparison = (DataQualityRelationComparison) element; + return comparison.testSuites == null || comparison.testSuites.isEmpty(); + } + + @Override + public TestRunner getTestRunner(Testable testable) + { + if (testable instanceof Root_meta_external_dataquality_DataQualityRelationComparison) + { + return new DataQualityRelationComparisonTestRunner((Root_meta_external_dataquality_DataQualityRelationComparison) testable, this.pureVersion); + } + return null; + } + + public void setPureVersion(String pureVersion) + { + this.pureVersion = pureVersion; + } +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestRunner.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestRunner.java new file mode 100644 index 00000000000..58f0cb839aa --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestRunner.java @@ -0,0 +1,394 @@ +// Copyright 2026 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.testable.dataquality.extension; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.map.MutableMap; +import org.eclipse.collections.api.tuple.Pair; +import org.eclipse.collections.impl.tuple.Tuples; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.ConnectionFirstPassBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.HelperModelBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.data.core.EmbeddedDataCompilerHelper; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.StoreProviderCompilerHelper; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.RelationAccessorTestConnectionFactory; +import org.finos.legend.engine.generation.dataquality.DataQualityLambdaGenerator; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.execution.result.Result; +import org.finos.legend.engine.plan.execution.result.serialization.SerializationFormat; +import org.finos.legend.engine.plan.generation.PlanGenerator; +import org.finos.legend.engine.plan.generation.extension.PlanGeneratorExtension; +import org.finos.legend.engine.plan.platform.PlanPlatform; +import org.finos.legend.engine.protocol.dataquality.metamodel.DataqualityRelationValidation; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTest; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestData; +import org.finos.legend.engine.protocol.dataquality.metamodel.testable.DataQualityRelationValidationTestSuite; +import org.finos.legend.engine.protocol.pure.v1.extension.ConnectionFactoryExtension; +import org.finos.legend.engine.protocol.pure.v1.extension.TestConnectionBuildParameters; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.data.DataElementReference; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedData; +import org.finos.legend.engine.protocol.pure.v1.model.data.relation.RelationElementsData; +import org.finos.legend.engine.protocol.pure.v1.model.executionPlan.SingleExecutionPlan; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.Connection; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestData; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.Store; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.AssertionStatus; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestExecuted; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestResult; +import org.finos.legend.engine.pure.code.core.PureCoreExtensionLoader; +import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; +import org.finos.legend.engine.testable.assertion.TestAssertionEvaluator; +import org.finos.legend.engine.testable.extension.AbstractTestSuiteSessionWithResources; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.extension.TestSuiteSession; +import org.finos.legend.engine.testable.helper.TestResultHelper; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Connection; +import org.finos.legend.pure.generated.Root_meta_core_runtime_ConnectionStore; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidation; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidationTestSuite; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; +import org.finos.legend.pure.generated.Root_meta_pure_test_TestSuite; +import org.finos.legend.pure.generated.core_pure_corefunctions_metaExtension; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.PackageableElement; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.FunctionDefinition; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.LambdaFunction; + +import java.io.Closeable; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Consumer; + +public class DataQualityRelationValidationTestRunner implements TestRunner +{ + private final Root_meta_external_dataquality_DataQualityRelationValidation pureElement; + private final MutableList extensions; + private final MutableList connectionBuilders = + Lists.mutable.withAll(ServiceLoader.load(ConnectionFactoryExtension.class)); + private final MutableList connectionAndDatabaseBuilders = + Lists.mutable.withAll(ServiceLoader.load(RelationAccessorTestConnectionFactory.class)); + private final PlanExecutor executor; + private final String pureVersion; + + public DataQualityRelationValidationTestRunner(Root_meta_external_dataquality_DataQualityRelationValidation pureElement, String pureVersion) + { + this.pureElement = pureElement; + this.pureVersion = pureVersion; + this.executor = PlanExecutor.newPlanExecutorBuilder().withAvailableStoreExecutors().build(); + this.extensions = Lists.mutable.withAll(ServiceLoader.load(PlanGeneratorExtension.class)); + } + + @Override + public TestResult executeAtomicTest(Root_meta_pure_test_AtomicTest atomicTest, PureModel pureModel, PureModelContextData data) + { + throw new UnsupportedOperationException("DataQualityRelationValidation Test should be executed in the context of a Test Suite only"); + } + + @Override + public TestSuiteSession openTestSuiteSession(Root_meta_pure_test_TestSuite testSuite, PureModel pureModel, PureModelContextData data) + { + if (!(testSuite instanceof Root_meta_external_dataquality_DataQualityRelationValidationTestSuite)) + { + throw new EngineException("Expected DataQualityRelationValidationTestSuite. Found: " + testSuite.getClass().getSimpleName()); + } + String testablePath = HelperModelBuilder.getElementFullPath(this.pureElement, pureModel.getExecutionSupport()); + DataqualityRelationValidation protocolElement = ListIterate.detect(data.getElementsOfType(DataqualityRelationValidation.class), e -> e.getPath().equals(testablePath)); + if (protocolElement == null) + { + throw new EngineException("Cannot find DataqualityRelationValidation '" + testablePath + "' in the model context"); + } + DataQualityRelationValidationTestSuite protocolSuite = ListIterate.detect(protocolElement.testSuites, s -> s.id.equals(testSuite._id())); + return new DataQualityRelationValidationTestSuiteSession(testSuite, protocolSuite, pureModel, data, testablePath); + } + + private class DataQualityRelationValidationTestSuiteSession + extends AbstractTestSuiteSessionWithResources + { + private final String testablePath; + private FunctionDefinition effectiveLambda; + + DataQualityRelationValidationTestSuiteSession( + Root_meta_pure_test_TestSuite pureSuite, + DataQualityRelationValidationTestSuite protocolSuite, + PureModel pureModel, + PureModelContextData pmcd, + String testablePath) + { + super(pureSuite, protocolSuite, pureModel, pmcd, + DataQualityRelationValidationTestSuiteSession::getTestSuiteTests, + DataQualityRelationValidationTestSuiteSession::getAtomicTestId); + this.testablePath = testablePath; + } + + @Override + protected void initialize(Consumer closeableConsumer) + { + this.effectiveLambda = (FunctionDefinition) DataQualityRelationValidationTestRunner.this.pureElement._query(); + DataQualityRelationValidationTestData testData = this.protocolSuite.testData; + if (testData == null || testData.testData == null || testData.testData.isEmpty()) + { + return; + } + MutableList storeTestData = Lists.mutable.empty(); + MutableList nonStoreRelationTestData = Lists.mutable.empty(); + for (FunctionTestData td : testData.testData) + { + try + { + StoreProviderCompilerHelper.getStoreFromPackageableElementPointer( + td.packageableElementPointer, this.pureModel.getContext()); + storeTestData.add(td); + } + catch (Exception e) + { + nonStoreRelationTestData.add(td); + } + } + if (storeTestData.notEmpty() && nonStoreRelationTestData.notEmpty()) + { + throw new EngineException("Error in DataQualityRelationValidation test suite " + this.protocolSuite.id + + ". The combination of store and non-store relation test data is not supported"); + } + if (storeTestData.notEmpty()) + { + setupStoreTestData(storeTestData); + } + else + { + this.effectiveLambda = applyTestData(this.effectiveLambda, nonStoreRelationTestData, this.pureModel, this.pmcd); + } + } + + private void setupStoreTestData(List storeTestData) + { + Root_meta_core_runtime_Runtime runtime = getRuntimeFromLambda(this.effectiveLambda, this.pureModel); + if (runtime == null) + { + return; + } + MutableMap> connectionMap = Maps.mutable.empty(); + runtime._connectionStores().forEach(cs -> connectionMap.getIfAbsentPut(cs._connection(), Lists.mutable::empty).add(cs)); + List> originalConnections = Lists.mutable.empty(); + ConnectionFirstPassBuilder connectionVisitor = new ConnectionFirstPassBuilder(this.pureModel.getContext()); + connectionMap.forEachKeyValue((connection, connectionStores) -> + { + MutableMap storeEmbeddedDataMap = Maps.mutable.empty(); + connectionStores.forEach(connectionStore -> + { + Object element = connectionStore._element(); + if (!(element instanceof org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store)) + { + return; + } + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store metamodelStore = + (org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store) element; + String connectionStorePath = HelperModelBuilder.getElementFullPath( + metamodelStore, this.pureModel.getExecutionSupport()); + Optional matchingTestData = storeTestData.stream().filter(td -> + { + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store tdStore = + StoreProviderCompilerHelper.getStoreFromPackageableElementPointer( + td.packageableElementPointer, this.pureModel.getContext()); + return connectionStorePath.equals(HelperModelBuilder.getElementFullPath( + tdStore, this.pureModel.getExecutionSupport())); + }).findFirst(); + if (matchingTestData.isPresent()) + { + FunctionTestData resolved = matchingTestData.get(); + EmbeddedData data = resolved.data instanceof DataElementReference + ? EmbeddedDataCompilerHelper.getEmbeddedDataFromDataElement( + (DataElementReference) resolved.data, this.pmcd) + : resolved.data; + Store protocolStore = resolveProtocolStore(this.pmcd, connectionStorePath); + if (protocolStore != null) + { + storeEmbeddedDataMap.put(protocolStore, data); + } + } + }); + if (storeEmbeddedDataMap.isEmpty()) + { + return; + } + Pair> mockConnection = buildTestConnection(storeEmbeddedDataMap); + Root_meta_core_runtime_Connection compiledMock = mockConnection.getOne().accept(connectionVisitor); + connectionStores.forEach(cs -> + { + originalConnections.add(Tuples.pair(cs, cs._connection())); + cs._connection(compiledMock); + }); + if (mockConnection.getTwo() != null) + { + registerCloseables(mockConnection.getTwo()); + } + }); + // Restore the runtime's original connections when the suite session closes so we don't + // leave the metamodel in a mutated state for other consumers of the same PureModel. + registerCloseable(() -> originalConnections.forEach(p -> p.getOne()._connection(p.getTwo()))); + } + + private Pair> buildTestConnection(MutableMap storeEmbeddedDataMap) + { + TestConnectionBuildParameters hints = TestConnectionBuildParameters.newBuilder().withIsRelation(true).build(); + for (ConnectionFactoryExtension factory : DataQualityRelationValidationTestRunner.this.connectionBuilders) + { + Optional>> optional = factory.tryBuildConnectionForStoreData( + Collections.emptyMap(), storeEmbeddedDataMap, hints); + if (optional != null && optional.isPresent()) + { + return optional.get(); + } + } + throw new EngineException("No ConnectionFactoryExtension could build a test connection for the " + + "DataQualityRelationValidation test suite " + this.protocolSuite.id); + } + + private Store resolveProtocolStore(PureModelContextData pmcd, String path) + { + return ListIterate.detect(pmcd.getElementsOfType(Store.class), s -> s.getPath().equals(path)); + } + + @Override + protected TestResult buildErrorResult(String atomicTestId, Throwable t) + { + return TestResultHelper.newTestError(this.testablePath, getTestSuiteId(), atomicTestId, t); + } + + @Override + protected TestResult runAtomicTest(DataQualityRelationValidationTest atomicTest) + { + TestResult testResult = executeValidationTest(atomicTest); + testResult.testable = this.testablePath; + testResult.testSuiteId = getTestSuiteId(); + return testResult; + } + + private TestResult executeValidationTest(DataQualityRelationValidationTest atomicTest) + { + try + { + LambdaFunction breaksLambda = buildBreaksLambda(); + SingleExecutionPlan plan = PlanGenerator.generateExecutionPlan( + breaksLambda, + null, + null, + null, + this.pureModel, + DataQualityRelationValidationTestRunner.this.pureVersion, + PlanPlatform.JAVA, + null, + PureCoreExtensionLoader.extensions().flatCollect(e -> e.extraPureCoreExtensions(this.pureModel.getExecutionSupport())), + DataQualityRelationValidationTestRunner.this.extensions.flatCollect(PlanGeneratorExtension::getExtraPlanTransformers)); + + Result result = DataQualityRelationValidationTestRunner.this.executor.execute(plan); + TestAssertion assertion = atomicTest.assertions.get(0); + AssertionStatus assertionStatus = assertion.accept(new TestAssertionEvaluator(result, SerializationFormat.RAW)); + TestExecuted testResult = new TestExecuted(Collections.singletonList(assertionStatus)); + testResult.atomicTestId = atomicTest.id; + return testResult; + } + catch (Exception e) + { + return TestResultHelper.newTestError(atomicTest.id, e); + } + } + + @SuppressWarnings("unchecked") + private LambdaFunction buildBreaksLambda() + { + Root_meta_external_dataquality_DataQualityRelationValidation element = + DataQualityRelationValidationTestRunner.this.pureElement; + LambdaFunction originalQuery = (LambdaFunction) element._query(); + LambdaFunction queryForGeneration = + (this.effectiveLambda == null || this.effectiveLambda == originalQuery) + ? originalQuery + : (LambdaFunction) this.effectiveLambda; + boolean swapped = queryForGeneration != originalQuery; + if (swapped) + { + element._query(queryForGeneration); + } + try + { + return DataQualityLambdaGenerator.generateLambda( + this.pureModel, element, null, false, null, false, false, null); + } + finally + { + if (swapped) + { + element._query(originalQuery); + } + } + } + } + + private static Root_meta_core_runtime_Runtime getRuntimeFromLambda(FunctionDefinition lambda, PureModel pureModel) + { + RichIterable runtimes = + core_pure_corefunctions_metaExtension.Root_meta_pure_functions_meta_extractRuntimesFromFunctionDefinition_FunctionDefinition_1__Runtime_MANY_( + lambda, pureModel.getExecutionSupport()); + switch (runtimes.size()) + { + case 0: + return null; + case 1: + return runtimes.getAny(); + default: + throw new EngineException("Cannot run DataQualityRelationValidation tests when the query references more than one runtime"); + } + } + + private FunctionDefinition applyTestData(FunctionDefinition lambda, List testData, PureModel pureModel, PureModelContextData pmcd) + { + MutableMap relationData = Maps.mutable.empty(); + for (FunctionTestData td : testData) + { + EmbeddedData data = td.data instanceof DataElementReference + ? EmbeddedDataCompilerHelper.getEmbeddedDataFromDataElement((DataElementReference) td.data, pmcd) + : td.data; + if (data instanceof RelationElementsData) + { + PackageableElement element = pureModel.getPackageableElement(td.packageableElementPointer.path); + relationData.put(element, (RelationElementsData) data); + } + } + if (relationData.isEmpty()) + { + return lambda; + } + MutableList> rewritten = this.connectionAndDatabaseBuilders + .collect(f -> f.rewriteFunctionForTestDataExecution(lambda, relationData, pureModel)); + rewritten.removeIf(Objects::isNull); + if (rewritten.isEmpty()) + { + throw new EngineException("No RelationAccessorTestConnectionFactory could handle the test data for the DataQualityRelationValidation lambda"); + } + if (rewritten.size() > 1) + { + throw new EngineException("Multiple RelationAccessorTestConnectionFactory implementations rewrote the DataQualityRelationValidation lambda; expected exactly one match"); + } + return rewritten.get(0); + } +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestableRunnerExtension.java b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestableRunnerExtension.java new file mode 100644 index 00000000000..524959bb823 --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/java/org/finos/legend/engine/testable/dataquality/extension/DataQualityRelationValidationTestableRunnerExtension.java @@ -0,0 +1,71 @@ +// Copyright 2026 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.testable.dataquality.extension; + +import org.eclipse.collections.api.list.MutableList; +import org.finos.legend.engine.protocol.dataquality.metamodel.DataqualityRelationValidation; +import org.finos.legend.engine.protocol.pure.PureClientVersions; +import org.finos.legend.engine.protocol.pure.m3.PackageableElement; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.extension.TestableRunnerExtension; +import org.finos.legend.pure.generated.Root_meta_external_dataquality_DataQualityRelationValidation; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.testable.Testable; + +public class DataQualityRelationValidationTestableRunnerExtension implements TestableRunnerExtension +{ + public static final String CLASSIFIER_PATH = "meta::external::dataquality::DataQualityRelationValidation"; + + private String pureVersion = PureClientVersions.production; + + @Override + public MutableList group() + { + return org.eclipse.collections.impl.factory.Lists.mutable.with("PackageableElement", "DataQualityValidation"); + } + + @Override + public String getSupportedClassifierPath() + { + return CLASSIFIER_PATH; + } + + @Override + public Boolean isTestable(PackageableElement element) + { + return element instanceof DataqualityRelationValidation; + } + + @Override + public Boolean isTestableEmpty(PackageableElement element) + { + DataqualityRelationValidation validation = (DataqualityRelationValidation) element; + return validation.testSuites == null || validation.testSuites.isEmpty(); + } + + @Override + public TestRunner getTestRunner(Testable testable) + { + if (testable instanceof Root_meta_external_dataquality_DataQualityRelationValidation) + { + return new DataQualityRelationValidationTestRunner((Root_meta_external_dataquality_DataQualityRelationValidation) testable, this.pureVersion); + } + return null; + } + + public void setPureVersion(String pureVersion) + { + this.pureVersion = pureVersion; + } +} diff --git a/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension new file mode 100644 index 00000000000..f03ad490c7d --- /dev/null +++ b/legend-engine-xts-dataquality/legend-engine-xt-dataquality-test-runner/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension @@ -0,0 +1,2 @@ +org.finos.legend.engine.testable.dataquality.extension.DataQualityRelationValidationTestableRunnerExtension +org.finos.legend.engine.testable.dataquality.extension.DataQualityRelationComparisonTestableRunnerExtension