diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java index 3eb5fcaaf8b..2d3bbb858d5 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java @@ -228,6 +228,12 @@ public class DataContractRepository extends EntityRepository { case Entity.TOPIC: failedFields = validateFieldsAgainstTopic(dataContract, entityRef); break; + case Entity.API_ENDPOINT: + failedFields = validateFieldsAgainstApiEndpoint(dataContract, entityRef); + break; + case Entity.DASHBOARD_DATA_MODEL: + failedFields = validateFieldsAgainstDashboardDataModel(dataContract, entityRef); + break; default: break; } @@ -245,52 +251,98 @@ public class DataContractRepository extends EntityRepository { private List validateFieldsAgainstTable( DataContract dataContract, EntityReference tableRef) { - List failedFields = new ArrayList<>(); org.openmetadata.schema.entity.data.Table table = Entity.getEntity(Entity.TABLE, tableRef.getId(), "columns", Include.NON_DELETED); if (table.getColumns() == null || table.getColumns().isEmpty()) { - // If table has no columns, all contract fields fail validation - return dataContract.getSchema().stream() - .map(org.openmetadata.schema.type.Column::getName) - .collect(Collectors.toList()); + return getAllContractFieldNames(dataContract); } Set tableColumnNames = table.getColumns().stream().map(Column::getName).collect(Collectors.toSet()); - for (org.openmetadata.schema.type.Column column : dataContract.getSchema()) { - if (!tableColumnNames.contains(column.getName())) { - failedFields.add(column.getName()); - } - } - - return failedFields; + return validateContractFieldsAgainstNames(dataContract, tableColumnNames); } private List validateFieldsAgainstTopic( DataContract dataContract, EntityReference topicRef) { - List failedFields = new ArrayList<>(); Topic topic = Entity.getEntity(Entity.TOPIC, topicRef.getId(), "messageSchema", Include.NON_DELETED); if (topic.getMessageSchema() == null || topic.getMessageSchema().getSchemaFields() == null || topic.getMessageSchema().getSchemaFields().isEmpty()) { - // If topic has no schema, all contract fields fail validation - return dataContract.getSchema().stream() - .map(org.openmetadata.schema.type.Column::getName) - .collect(Collectors.toList()); + return getAllContractFieldNames(dataContract); } Set topicFieldNames = extractFieldNames(topic.getMessageSchema().getSchemaFields()); + return validateContractFieldsAgainstNames(dataContract, topicFieldNames); + } + + private List validateFieldsAgainstApiEndpoint( + DataContract dataContract, EntityReference apiEndpointRef) { + org.openmetadata.schema.entity.data.APIEndpoint apiEndpoint = + Entity.getEntity( + Entity.API_ENDPOINT, + apiEndpointRef.getId(), + "requestSchema,responseSchema", + Include.NON_DELETED); + + Set apiFieldNames = new HashSet<>(); + + if (apiEndpoint.getRequestSchema() != null + && apiEndpoint.getRequestSchema().getSchemaFields() != null + && !apiEndpoint.getRequestSchema().getSchemaFields().isEmpty()) { + apiFieldNames.addAll(extractFieldNames(apiEndpoint.getRequestSchema().getSchemaFields())); + } + + if (apiEndpoint.getResponseSchema() != null + && apiEndpoint.getResponseSchema().getSchemaFields() != null + && !apiEndpoint.getResponseSchema().getSchemaFields().isEmpty()) { + apiFieldNames.addAll(extractFieldNames(apiEndpoint.getResponseSchema().getSchemaFields())); + } + + if (apiFieldNames.isEmpty()) { + return getAllContractFieldNames(dataContract); + } + + return validateContractFieldsAgainstNames(dataContract, apiFieldNames); + } + + private List validateFieldsAgainstDashboardDataModel( + DataContract dataContract, EntityReference dashboardDataModelRef) { + org.openmetadata.schema.entity.data.DashboardDataModel dashboardDataModel = + Entity.getEntity( + Entity.DASHBOARD_DATA_MODEL, + dashboardDataModelRef.getId(), + "columns", + Include.NON_DELETED); + + if (dashboardDataModel.getColumns() == null || dashboardDataModel.getColumns().isEmpty()) { + return getAllContractFieldNames(dataContract); + } + + Set dataModelColumnNames = + dashboardDataModel.getColumns().stream().map(Column::getName).collect(Collectors.toSet()); + + return validateContractFieldsAgainstNames(dataContract, dataModelColumnNames); + } + + private List getAllContractFieldNames(DataContract dataContract) { + return dataContract.getSchema().stream() + .map(org.openmetadata.schema.type.Column::getName) + .collect(Collectors.toList()); + } + + private List validateContractFieldsAgainstNames( + DataContract dataContract, Set entityFieldNames) { + List failedFields = new ArrayList<>(); for (org.openmetadata.schema.type.Column column : dataContract.getSchema()) { - if (!topicFieldNames.contains(column.getName())) { + if (!entityFieldNames.contains(column.getName())) { failedFields.add(column.getName()); } } - return failedFields; } @@ -369,6 +421,7 @@ public class DataContractRepository extends EntityRepository { Entity.DATABASE, Entity.DATABASE_SCHEMA, Entity.DASHBOARD, + Entity.CHART, Entity.DASHBOARD_DATA_MODEL, Entity.PIPELINE, Entity.TOPIC, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java index 6de4d4a84b8..c0403d5e7ff 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java @@ -71,6 +71,7 @@ import org.openmetadata.schema.api.services.CreateMessagingService; import org.openmetadata.schema.api.services.DatabaseConnection; import org.openmetadata.schema.api.tests.CreateTestCase; import org.openmetadata.schema.entity.data.APIEndpoint; +import org.openmetadata.schema.entity.data.Chart; import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.data.DashboardDataModel; import org.openmetadata.schema.entity.data.DataContract; @@ -108,12 +109,14 @@ import org.openmetadata.sdk.PipelineServiceClientInterface; import org.openmetadata.service.jdbi3.DataContractRepository; import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.apis.APIEndpointResourceTest; +import org.openmetadata.service.resources.charts.ChartResourceTest; import org.openmetadata.service.resources.dashboards.DashboardResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest; import org.openmetadata.service.resources.datamodels.DashboardDataModelResourceTest; import org.openmetadata.service.resources.dqtests.TestCaseResourceTest; import org.openmetadata.service.resources.dqtests.TestSuiteResourceTest; import org.openmetadata.service.resources.services.ingestionpipelines.IngestionPipelineResourceTest; +import org.openmetadata.service.resources.topics.TopicResourceTest; import org.openmetadata.service.security.SecurityUtil; import org.openmetadata.service.util.TestUtils; @@ -138,6 +141,7 @@ public class DataContractResourceTest extends EntityResourceTest customFields) throws IOException { // Ensure we have a messaging service to work with String messagingServiceName = ensureMessagingService(); @@ -610,24 +622,27 @@ public class DataContractResourceTest extends EntityResourceTest fields = - List.of( - new Field() - .withName("messageId") - .withDisplayName("Message ID") - .withDataType(FieldDataType.STRING), - new Field() - .withName("eventType") - .withDisplayName("Event Type") - .withDataType(FieldDataType.STRING), - new Field() - .withName("payload") - .withDisplayName("Payload") - .withDataType(FieldDataType.STRING), - new Field() - .withName("timestamp") - .withDisplayName("Timestamp") - .withDataType(FieldDataType.TIMESTAMP)); + List fields = customFields; + if (fields == null) { + fields = + List.of( + new Field() + .withName("messageId") + .withDisplayName("Message ID") + .withDataType(FieldDataType.STRING), + new Field() + .withName("eventType") + .withDisplayName("Event Type") + .withDataType(FieldDataType.STRING), + new Field() + .withName("payload") + .withDisplayName("Payload") + .withDataType(FieldDataType.STRING), + new Field() + .withName("timestamp") + .withDisplayName("Timestamp") + .withDataType(FieldDataType.TIMESTAMP)); + } MessageSchema messageSchema = new MessageSchema() @@ -657,6 +672,17 @@ public class DataContractResourceTest extends EntityResourceTest requestFields, + List responseFields) + throws IOException { // Use multiple entropy sources for absolute uniqueness long counter = tableCounter.incrementAndGet(); long timestamp = System.nanoTime(); @@ -675,6 +701,19 @@ public class DataContractResourceTest extends EntityResourceTest columns) throws IOException { // Use multiple entropy sources for absolute uniqueness long counter = tableCounter.incrementAndGet(); long timestamp = System.nanoTime(); @@ -707,6 +754,11 @@ public class DataContractResourceTest extends EntityResourceTest columns = + List.of( + new Column() + .withName("testField") + .withDescription("Test field") + .withDataType(ColumnDataType.STRING)); + + CreateDataContract createWithSchema = + createDataContractRequestForEntity(test.getDisplayName() + "_schema", chart) + .withSchema(columns); + + assertResponseContains( + () -> createDataContract(createWithSchema), + BAD_REQUEST, + "Schema validation is not supported for chart entities. Only table, topic, apiEndpoint, and dashboardDataModel entities support schema validation"); + + // Test 2: Chart with semantics validation only should succeed + // Create semantics rules only (no quality expectations or schema) + List semanticsRules = + List.of( + new SemanticsRule() + .withName("Chart Display Name Check") + .withDescription("Ensures chart has a valid display name") + .withRule("{ \"!!\": { \"var\": \"displayName\" } }"), + new SemanticsRule() + .withName("Chart Type Check") + .withDescription("Ensures chart has a valid chart type") + .withRule("{ \"!!\": { \"var\": \"chartType\" } }")); + + // Create data contract for the chart with semantics rules only + CreateDataContract create = + createDataContractRequestForEntity(test.getDisplayName(), chart) + .withDescription("Data contract for chart with semantics validation") + .withSemantics(semanticsRules) + .withEntityStatus(EntityStatus.APPROVED); + + DataContract dataContract = createDataContract(create); + + // Verify the data contract was created successfully + assertNotNull(dataContract); + assertNotNull(dataContract.getId()); + assertEquals(create.getName(), dataContract.getName()); + assertEquals(create.getEntityStatus(), dataContract.getEntityStatus()); + assertEquals(chart.getId(), dataContract.getEntity().getId()); + assertEquals("chart", dataContract.getEntity().getType()); + + // Verify semantics rules are properly set + assertNotNull(dataContract.getSemantics()); + assertEquals(2, dataContract.getSemantics().size()); + assertSemantics(create.getSemantics(), dataContract.getSemantics()); + + // Verify no quality expectations or schema are set (semantics only) + assertNull(dataContract.getQualityExpectations()); + assertNull(dataContract.getSchema()); + + // Verify FQN follows expected pattern + String expectedFQN = chart.getFullyQualifiedName() + ".dataContract_" + create.getName(); + assertEquals(expectedFQN, dataContract.getFullyQualifiedName()); + + // Test the validate method and verify contract status + DataContractResult validationResult = runValidate(dataContract); + + // Verify the validation result + assertNotNull(validationResult); + assertNotNull(validationResult.getContractExecutionStatus()); + + // Verify semantics validation was performed + assertNotNull(validationResult.getSemanticsValidation()); + assertEquals(2, validationResult.getSemanticsValidation().getTotal().intValue()); + + assertTrue(validationResult.getSemanticsValidation().getTotal() > 0); + + // Verify no schema or quality validation was performed (semantics only) + assertNull(validationResult.getSchemaValidation()); + assertNull(validationResult.getQualityValidation()); + + // Retrieve the contract and verify the latest result is stored + DataContract retrievedContract = getDataContract(dataContract.getId(), ""); + assertNotNull(retrievedContract.getLatestResult()); + assertEquals(validationResult.getId(), retrievedContract.getLatestResult().getResultId()); + } + @Test @Execution(ExecutionMode.CONCURRENT) void testDashboardEntityConstraints(TestInfo test) throws IOException { @@ -3720,8 +3863,20 @@ public class DataContractResourceTest extends EntityResourceTest messageSchemaFields = + List.of( + new Field() + .withName("messageId") + .withDisplayName("Message ID") + .withDataType(FieldDataType.STRING), + new Field() + .withName("eventType") + .withDisplayName("Event Type") + .withDataType(FieldDataType.STRING)); + // Test 1: Topic with schema should succeed (topics support schema validation) - Topic schemaTopic = createUniqueTopic(test.getDisplayName() + "_schema"); + Topic schemaTopic = createUniqueTopic(test.getDisplayName() + "_schema", messageSchemaFields); List columns = List.of( @@ -3748,10 +3903,15 @@ public class DataContractResourceTest extends EntityResourceTest messageSchemaFields = + List.of( + new Field() + .withName("messageId") + .withDisplayName("Message ID") + .withDataType(FieldDataType.STRING), + new Field() + .withName("eventType") + .withDisplayName("Event Type") + .withDataType(FieldDataType.STRING)); + + Topic schemaTopic = + createUniqueTopic(test.getDisplayName() + "_schema_fail", messageSchemaFields); + + // Create data contract with schema fields that match the topic's message schema + List columns = + List.of( + new Column() + .withName("messageId") + .withDescription("Message ID") + .withDataType(ColumnDataType.STRING), + new Column() + .withName("eventType") + .withDescription("Event Type") + .withDataType(ColumnDataType.STRING)); + + CreateDataContract createWithSchema = + createDataContractRequestForEntity(test.getDisplayName() + "_schema_fail", schemaTopic) + .withSchema(columns); + + // Create data contract with schema that matches the topic's message schema + DataContract schemaContract = createDataContract(createWithSchema); + assertNotNull(schemaContract); + assertNotNull(schemaContract.getSchema()); + assertEquals(2, schemaContract.getSchema().size()); + + // First, validate the contract when the schema is still valid - should pass + DataContractResult initialResult = runValidate(schemaContract); + assertNotNull(initialResult); + assertEquals(ContractExecutionStatus.Success, initialResult.getContractExecutionStatus()); + assertEquals(0, initialResult.getSchemaValidation().getFailed().intValue()); + assertEquals(2, initialResult.getSchemaValidation().getPassed().intValue()); + + // Now let's "break" the topic by removing one of the fields that the contract expects + // We'll simulate this by updating the topic's message schema to have fewer fields + String originalTopicJson = JsonUtils.pojoToJson(schemaTopic); + + // Remove the "eventType" field from the topic's message schema + List updatedFields = new ArrayList<>(); + for (Field field : schemaTopic.getMessageSchema().getSchemaFields()) { + if (!"eventType".equals(field.getName())) { + updatedFields.add(field); + } + } + MessageSchema updatedMessageSchema = + schemaTopic.getMessageSchema().withSchemaFields(updatedFields); + schemaTopic.setMessageSchema(updatedMessageSchema); + + // Patch the topic to remove the eventType field using TopicResourceTest + TopicResourceTest topicResourceTest = new TopicResourceTest(); + Topic patchedTopic = + topicResourceTest.patchEntity( + schemaTopic.getId(), originalTopicJson, schemaTopic, ADMIN_AUTH_HEADERS); + + // Verify the eventType field was removed from message schema + assertEquals(1, patchedTopic.getMessageSchema().getSchemaFields().size()); + assertNull( + patchedTopic.getMessageSchema().getSchemaFields().stream() + .filter(field -> "eventType".equals(field.getName())) + .findFirst() + .orElse(null)); + + // Now validate the data contract - it should fail schema validation + DataContractResult result = runValidate(schemaContract); + + // Verify the validation result shows failure due to schema validation + assertNotNull(result); + assertEquals(ContractExecutionStatus.Failed, result.getContractExecutionStatus()); + + // Verify schema validation details + assertNotNull(result.getSchemaValidation()); + assertEquals( + 1, result.getSchemaValidation().getFailed().intValue()); // 1 field failed (eventType) + assertEquals( + 1, result.getSchemaValidation().getPassed().intValue()); // 1 field passed (messageId) + assertEquals( + 2, result.getSchemaValidation().getTotal().intValue()); // 2 total fields in contract + + // Verify the failed field is the eventType field + assertNotNull(result.getSchemaValidation().getFailedFields()); + assertEquals(1, result.getSchemaValidation().getFailedFields().size()); + assertEquals("eventType", result.getSchemaValidation().getFailedFields().get(0)); + } + @Test @Execution(ExecutionMode.CONCURRENT) void testApiEndpointEntityConstraints(TestInfo test) throws IOException { // Test 1: API Endpoint with schema should succeed (apiEndpoint supports schema validation) - APIEndpoint schemaApiEndpoint = createUniqueApiEndpoint(test.getDisplayName() + "_schema"); + // Create schema fields for the API endpoint that match what the data contract expects + List requestSchemaFields = + List.of( + new Field() + .withName("requestId") + .withDataType(FieldDataType.STRING) + .withDescription("Request ID")); + List responseSchemaFields = + List.of( + new Field() + .withName("responseCode") + .withDataType(FieldDataType.INT) + .withDescription("Response Code")); + + APIEndpoint schemaApiEndpoint = + createUniqueApiEndpoint( + test.getDisplayName() + "_schema", requestSchemaFields, responseSchemaFields); + List columns = List.of( new Column() @@ -3852,10 +4127,15 @@ public class DataContractResourceTest extends EntityResourceTest requestSchemaFields = + List.of( + new Field() + .withName("requestId") + .withDataType(FieldDataType.STRING) + .withDescription("Request ID")); + List responseSchemaFields = + List.of( + new Field() + .withName("responseCode") + .withDataType(FieldDataType.INT) + .withDescription("Response Code")); + + APIEndpoint schemaApiEndpoint = + createUniqueApiEndpoint( + test.getDisplayName() + "_schema", requestSchemaFields, responseSchemaFields); + + List columns = + List.of( + new Column() + .withName("requestId") + .withDescription("Request ID") + .withDataType(ColumnDataType.STRING), + new Column() + .withName("responseCode") + .withDescription("Response Code") + .withDataType(ColumnDataType.INT)); + + CreateDataContract createWithSchema = + createDataContractRequestForEntity(test.getDisplayName() + "_schema", schemaApiEndpoint) + .withSchema(columns); + + // Create data contract with schema that matches the API endpoint + DataContract schemaContract = createDataContract(createWithSchema); + assertNotNull(schemaContract); + assertNotNull(schemaContract.getSchema()); + assertEquals(2, schemaContract.getSchema().size()); + + // First, validate the contract when the schema is still valid - should pass + DataContractResult initialResult = runValidate(schemaContract); + assertNotNull(initialResult); + assertEquals(ContractExecutionStatus.Success, initialResult.getContractExecutionStatus()); + assertEquals(0, initialResult.getSchemaValidation().getFailed().intValue()); + assertEquals(2, initialResult.getSchemaValidation().getPassed().intValue()); + + // Now let's "break" the API endpoint by removing one of the fields that the contract expects + // We'll simulate this by updating the API endpoint to have fewer response fields + String originalApiEndpointJson = JsonUtils.pojoToJson(schemaApiEndpoint); + + // Remove the "responseCode" field from the response schema + List updatedResponseFields = new ArrayList<>(); + for (Field field : schemaApiEndpoint.getResponseSchema().getSchemaFields()) { + if (!"responseCode".equals(field.getName())) { + updatedResponseFields.add(field); + } + } + schemaApiEndpoint.getResponseSchema().setSchemaFields(updatedResponseFields); + + // Patch the API endpoint to remove the responseCode field using APIEndpointResourceTest + APIEndpointResourceTest apiEndpointResourceTest = new APIEndpointResourceTest(); + APIEndpoint patchedApiEndpoint = + apiEndpointResourceTest.patchEntity( + schemaApiEndpoint.getId(), + originalApiEndpointJson, + schemaApiEndpoint, + ADMIN_AUTH_HEADERS); + + // Verify the responseCode field was removed from response schema + assertEquals(0, patchedApiEndpoint.getResponseSchema().getSchemaFields().size()); + assertNull( + patchedApiEndpoint.getResponseSchema().getSchemaFields().stream() + .filter(field -> "responseCode".equals(field.getName())) + .findFirst() + .orElse(null)); + + // Now validate the data contract - it should fail schema validation + DataContractResult result = runValidate(schemaContract); + + // Verify the validation result shows failure due to schema validation + assertNotNull(result); + assertEquals(ContractExecutionStatus.Failed, result.getContractExecutionStatus()); + + // Verify schema validation details + assertNotNull(result.getSchemaValidation()); + assertEquals( + 1, result.getSchemaValidation().getFailed().intValue()); // 1 field failed (responseCode) + assertEquals( + 1, result.getSchemaValidation().getPassed().intValue()); // 1 field passed (requestId) + assertEquals( + 2, result.getSchemaValidation().getTotal().intValue()); // 2 total fields in contract + + // Verify the failed field is the responseCode field + assertNotNull(result.getSchemaValidation().getFailedFields()); + assertEquals(1, result.getSchemaValidation().getFailedFields().size()); + assertEquals("responseCode", result.getSchemaValidation().getFailedFields().get(0)); + } + @Test @Execution(ExecutionMode.CONCURRENT) void testDashboardDataModelEntityConstraints(TestInfo test) throws IOException { // Test 1: Dashboard Data Model with schema should succeed (dashboardDataModel supports schema // validation) - DashboardDataModel schemaDataModel = - createUniqueDashboardDataModel(test.getDisplayName() + "_schema"); + // Create columns that match what the data contract will expect + List dataModelColumns = + List.of( + new org.openmetadata.schema.type.Column() + .withName("metricId") + .withDescription("Metric ID") + .withDataType(org.openmetadata.schema.type.ColumnDataType.STRING), + new org.openmetadata.schema.type.Column() + .withName("metricValue") + .withDescription("Metric Value") + .withDataType(org.openmetadata.schema.type.ColumnDataType.DOUBLE)); + + // Create corresponding data contract columns List columns = List.of( new Column() @@ -3939,6 +4333,10 @@ public class DataContractResourceTest extends EntityResourceTest dataModelColumns = + List.of( + new org.openmetadata.schema.type.Column() + .withName("metricId") + .withDescription("Metric ID") + .withDataType(org.openmetadata.schema.type.ColumnDataType.STRING), + new org.openmetadata.schema.type.Column() + .withName("metricValue") + .withDescription("Metric Value") + .withDataType(org.openmetadata.schema.type.ColumnDataType.DOUBLE)); + + // Create corresponding data contract columns + List columns = + List.of( + new Column() + .withName("metricId") + .withDescription("Metric ID") + .withDataType(ColumnDataType.STRING), + new Column() + .withName("metricValue") + .withDescription("Metric Value") + .withDataType(ColumnDataType.DOUBLE)); + + DashboardDataModel schemaDataModel = + createUniqueDashboardDataModel(test.getDisplayName() + "_schema", dataModelColumns); + + CreateDataContract createWithSchema = + createDataContractRequestForEntity(test.getDisplayName() + "_schema", schemaDataModel) + .withSchema(columns); + + // Create data contract with schema that matches the data model + DataContract schemaContract = createDataContract(createWithSchema); + assertNotNull(schemaContract); + assertNotNull(schemaContract.getSchema()); + assertEquals(2, schemaContract.getSchema().size()); + + // First, validate the contract when the schema is still valid - should pass + DataContractResult initialResult = runValidate(schemaContract); + assertNotNull(initialResult); + assertEquals(ContractExecutionStatus.Success, initialResult.getContractExecutionStatus()); + assertEquals(0, initialResult.getSchemaValidation().getFailed().intValue()); + assertEquals(2, initialResult.getSchemaValidation().getPassed().intValue()); + + // Now let's "break" the data model by removing one of the columns that the contract expects + // We'll simulate this by updating the data model to have fewer columns + String originalDataModelJson = JsonUtils.pojoToJson(schemaDataModel); + + // Remove the "metricValue" column from the data model columns + List updatedColumns = new ArrayList<>(); + for (org.openmetadata.schema.type.Column col : schemaDataModel.getColumns()) { + if (!"metricValue".equals(col.getName())) { + updatedColumns.add(col); + } + } + schemaDataModel.setColumns(updatedColumns); + + // Patch the data model to remove the metricValue column using DashboardDataModelResourceTest + DashboardDataModelResourceTest dataModelResourceTest = new DashboardDataModelResourceTest(); + DashboardDataModel patchedDataModel = + dataModelResourceTest.patchEntity( + schemaDataModel.getId(), originalDataModelJson, schemaDataModel, ADMIN_AUTH_HEADERS); + + // Verify the metricValue column was removed + assertEquals(1, patchedDataModel.getColumns().size()); + assertNull( + patchedDataModel.getColumns().stream() + .filter(col -> "metricValue".equals(col.getName())) + .findFirst() + .orElse(null)); + + // Now validate the data contract - it should fail schema validation + DataContractResult result = runValidate(schemaContract); + + // Verify the validation result shows failure due to schema validation + assertNotNull(result); + assertEquals(ContractExecutionStatus.Failed, result.getContractExecutionStatus()); + + // Verify schema validation details + assertNotNull(result.getSchemaValidation()); + assertEquals( + 1, result.getSchemaValidation().getFailed().intValue()); // 1 field failed (metricValue) + assertEquals( + 1, result.getSchemaValidation().getPassed().intValue()); // 1 field passed (metricId) + assertEquals( + 2, result.getSchemaValidation().getTotal().intValue()); // 2 total fields in contract + + // Verify the failed field is the metricValue column + assertNotNull(result.getSchemaValidation().getFailedFields()); + assertEquals(1, result.getSchemaValidation().getFailedFields().size()); + assertEquals("metricValue", result.getSchemaValidation().getFailedFields().get(0)); + } + @Test @Execution(ExecutionMode.CONCURRENT) void testTableEntityConstraints(TestInfo test) throws IOException {