diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java index c0cec830faf2b..c6a48ea27cbf3 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGenerator.java @@ -21,7 +21,12 @@ import com.linkedin.schema.SchemaMetadata; import jakarta.json.JsonPatch; import java.net.URISyntaxException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -204,169 +209,140 @@ private static List getFieldPropertyChangeEvents( return propChangeEvents; } - private static Map getSchemaFieldMap(SchemaFieldArray fieldArray) { - Map fieldMap = new HashMap<>(); - fieldArray.forEach(schemaField -> fieldMap.put(schemaField.getFieldPath(), schemaField)); - return fieldMap; - } - - private static void processFieldPathDataTypeChange( - String baseFieldPath, - Urn datasetUrn, - ChangeCategory changeCategory, - AuditStamp auditStamp, - Map baseFieldMap, - Map targetFieldMap, - Set processedBaseFields, - Set processedTargetFields, - List changeEvents) { - SchemaField curBaseField = baseFieldMap.get(baseFieldPath); - if (!targetFieldMap.containsKey(baseFieldPath)) { - return; - } - processedBaseFields.add(baseFieldPath); - processedTargetFields.add(baseFieldPath); - SchemaField curTargetField = targetFieldMap.get(baseFieldPath); - if (!curBaseField.getNativeDataType().equals(curTargetField.getNativeDataType())) { - // Non-backward compatible change + Major version bump - if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { - changeEvents.add( - DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() - .category(ChangeCategory.TECHNICAL_SCHEMA) - .modifier(getSchemaFieldUrn(datasetUrn, curBaseField).toString()) - .entityUrn(datasetUrn.toString()) - .operation(ChangeOperation.MODIFY) - .semVerChange(SemanticChangeType.MAJOR) - .description( - String.format( - "%s native datatype of the field '%s' changed from '%s' to '%s'.", - BACKWARDS_INCOMPATIBLE_DESC, - getFieldPathV1(curTargetField), - curBaseField.getNativeDataType(), - curTargetField.getNativeDataType())) - .fieldPath(curBaseField.getFieldPath()) - .fieldUrn(getSchemaFieldUrn(datasetUrn, curBaseField)) - .nullable(curBaseField.isNullable()) - .modificationCategory(SchemaFieldModificationCategory.TYPE_CHANGE) - .auditStamp(auditStamp) - .build()); - } - List propChangeEvents = - getFieldPropertyChangeEvents( - curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); - changeEvents.addAll(propChangeEvents); - } - List propChangeEvents = - getFieldPropertyChangeEvents( - curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); - changeEvents.addAll(propChangeEvents); - } - - private static void processFieldPathRename( - String baseFieldPath, - Urn datasetUrn, - ChangeCategory changeCategory, - AuditStamp auditStamp, - Map baseFieldMap, - Map targetFieldMap, - Set processedBaseFields, - Set processedTargetFields, - List changeEvents, - Set renamedFields) { - - List nonProcessedTargetSchemaFields = new ArrayList<>(); - targetFieldMap.forEach( - (s, schemaField) -> { - if (!processedTargetFields.contains(s)) { - nonProcessedTargetSchemaFields.add(schemaField); - } - }); - - SchemaField curBaseField = baseFieldMap.get(baseFieldPath); - SchemaField renamedField = - findRenamedField(curBaseField, nonProcessedTargetSchemaFields, renamedFields); - processedBaseFields.add(baseFieldPath); - if (renamedField == null) { - processRemoval(changeCategory, changeEvents, datasetUrn, curBaseField, auditStamp); - } else { - if (!ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { - return; - } - processedTargetFields.add(renamedField.getFieldPath()); - changeEvents.add(generateRenameEvent(datasetUrn, curBaseField, renamedField, auditStamp)); - List propChangeEvents = - getFieldPropertyChangeEvents( - curBaseField, renamedField, datasetUrn, changeCategory, auditStamp); - changeEvents.addAll(propChangeEvents); - } - } - - private static Set getNonProcessedFields( - Map fieldMap, Set processedFields) { - Set nonProcessedFields = new HashSet<>(fieldMap.keySet()); - nonProcessedFields.removeAll(processedFields); - return nonProcessedFields; - } - private static List computeDiffs( SchemaMetadata baseSchema, SchemaMetadata targetSchema, Urn datasetUrn, ChangeCategory changeCategory, AuditStamp auditStamp) { + // Sort the fields by their field path. This aligns both sets of fields based on field paths for + // comparisons. + if (baseSchema != null) { + sortFieldsByPath(baseSchema); + } + if (targetSchema != null) { + sortFieldsByPath(targetSchema); + } + SchemaFieldArray baseFields = (baseSchema != null ? baseSchema.getFields() : new SchemaFieldArray()); SchemaFieldArray targetFields = targetSchema != null ? targetSchema.getFields() : new SchemaFieldArray(); - - Map baseFieldMap = getSchemaFieldMap(baseFields); - Map targetFieldMap = getSchemaFieldMap(targetFields); - - Set processedBaseFields = new HashSet<>(); - Set processedTargetFields = new HashSet<>(); - + int baseFieldIdx = 0; + int targetFieldIdx = 0; List changeEvents = new ArrayList<>(); Set renamedFields = new HashSet<>(); - for (String baseFieldPath : baseFieldMap.keySet()) { - processFieldPathDataTypeChange( - baseFieldPath, - datasetUrn, - changeCategory, - auditStamp, - baseFieldMap, - targetFieldMap, - processedBaseFields, - processedTargetFields, - changeEvents); + // Compares each sorted base field with the target field, tries to reconcile name changes by + // matching field properties + while (baseFieldIdx < baseFields.size() && targetFieldIdx < targetFields.size()) { + SchemaField curBaseField = baseFields.get(baseFieldIdx); + SchemaField curTargetField = targetFields.get(targetFieldIdx); + int comparison = curBaseField.getFieldPath().compareTo(curTargetField.getFieldPath()); + if (renamedFields.contains(curBaseField)) { + baseFieldIdx++; + } else if (renamedFields.contains(curTargetField)) { + targetFieldIdx++; + } else if (comparison == 0) { + // This is the same field. Check for change events from property changes. + if (!curBaseField.getNativeDataType().equals(curTargetField.getNativeDataType())) { + processNativeTypeChange( + changeCategory, changeEvents, datasetUrn, curBaseField, curTargetField, auditStamp); + } + List propChangeEvents = + getFieldPropertyChangeEvents( + curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + changeEvents.addAll(propChangeEvents); + ++baseFieldIdx; + ++targetFieldIdx; + } else if (comparison < 0) { + // Base Field was removed or was renamed. Non-backward compatible change + Major version + // bump for removal + // Forwards/Backwards compatible change and Minor version bump for rename + // Check for rename, if rename coincides with other modifications we assume drop/add. + // Assumes that two different fields on the same schema would not have the same description, + // terms, and tags and share the same type + SchemaField renamedField = + findRenamedField( + curBaseField, + targetFields.subList(targetFieldIdx, targetFields.size()), + renamedFields); + if (renamedField == null) { + processRemoval(changeCategory, changeEvents, datasetUrn, curBaseField, auditStamp); + ++baseFieldIdx; + } else { + if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + changeEvents.add( + generateRenameEvent(datasetUrn, curBaseField, renamedField, auditStamp)); + } + List propChangeEvents = + getFieldPropertyChangeEvents( + curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + changeEvents.addAll(propChangeEvents); + ++baseFieldIdx; + renamedFields.add(renamedField); + } + } else { + // The targetField got added or a renaming occurred. Forward & backwards compatible change + + // minor version bump for both. + SchemaField renamedField = + findRenamedField( + curTargetField, baseFields.subList(baseFieldIdx, baseFields.size()), renamedFields); + if (renamedField == null) { + processAdd(changeCategory, changeEvents, datasetUrn, curTargetField, auditStamp); + ++targetFieldIdx; + } else { + if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + changeEvents.add( + generateRenameEvent(datasetUrn, renamedField, curTargetField, auditStamp)); + } + List propChangeEvents = + getFieldPropertyChangeEvents( + curBaseField, curTargetField, datasetUrn, changeCategory, auditStamp); + changeEvents.addAll(propChangeEvents); + ++targetFieldIdx; + renamedFields.add(renamedField); + } + } } - Set nonProcessedBaseFields = getNonProcessedFields(baseFieldMap, processedBaseFields); - for (String baseFieldPath : nonProcessedBaseFields) { - processFieldPathRename( - baseFieldPath, - datasetUrn, - changeCategory, - auditStamp, - baseFieldMap, - targetFieldMap, - processedBaseFields, - processedTargetFields, - changeEvents, - renamedFields); + while (baseFieldIdx < baseFields.size()) { + // Handle removed fields. Non-backward compatible change + major version bump + SchemaField baseField = baseFields.get(baseFieldIdx); + if (!renamedFields.contains(baseField)) { + processRemoval(changeCategory, changeEvents, datasetUrn, baseField, auditStamp); + } + ++baseFieldIdx; + } + while (targetFieldIdx < targetFields.size()) { + // Newly added fields. Forwards & backwards compatible change + minor version bump. + SchemaField targetField = targetFields.get(targetFieldIdx); + if (!renamedFields.contains(targetField)) { + processAdd(changeCategory, changeEvents, datasetUrn, targetField, auditStamp); + } + ++targetFieldIdx; } - Set nonProcessedTargetFields = - getNonProcessedFields(targetFieldMap, processedTargetFields); + // Handle primary key constraint change events. + List primaryKeyChangeEvents = + getPrimaryKeyChangeEvents(changeCategory, baseSchema, targetSchema, datasetUrn, auditStamp); + changeEvents.addAll(primaryKeyChangeEvents); - nonProcessedTargetFields.forEach( - fieldPath -> { - SchemaField curTargetField = targetFieldMap.get(fieldPath); - processAdd(changeCategory, changeEvents, datasetUrn, curTargetField, auditStamp); - }); + // Handle foreign key constraint change events, currently no-op due to field not being utilized. + List foreignKeyChangeEvents = getForeignKeyChangeEvents(); + changeEvents.addAll(foreignKeyChangeEvents); return changeEvents; } + private static void sortFieldsByPath(SchemaMetadata schemaMetadata) { + if (schemaMetadata == null) { + throw new IllegalArgumentException("SchemaMetadata should not be null"); + } + List schemaFields = new ArrayList<>(schemaMetadata.getFields()); + schemaFields.sort(Comparator.comparing(SchemaField::getFieldPath)); + schemaMetadata.setFields(new SchemaFieldArray(schemaFields)); + } + private static SchemaField findRenamedField( SchemaField curField, List targetFields, Set renamedFields) { return targetFields.stream() @@ -388,23 +364,14 @@ private static boolean parentFieldsMatch(SchemaField curField, SchemaField schem if (curFieldIndex > 0 && schemaFieldIndex > 0) { String curFieldParentPath = curField.getFieldPath().substring(0, curFieldIndex); String schemaFieldParentPath = schemaField.getFieldPath().substring(0, schemaFieldIndex); - return StringUtils.isNotBlank(curFieldParentPath) - && curFieldParentPath.equals(schemaFieldParentPath); + return StringUtils.equals(curFieldParentPath, schemaFieldParentPath); } // No parent field return curFieldIndex < 0 && schemaFieldIndex < 0; } private static boolean descriptionsMatch(SchemaField curField, SchemaField schemaField) { - if (StringUtils.isBlank(curField.getDescription()) - && StringUtils.isBlank(schemaField.getDescription())) { - return true; - } - return !(StringUtils.isBlank(curField.getDescription()) - && StringUtils.isNotBlank(schemaField.getDescription())) - && !(StringUtils.isNotBlank(curField.getDescription()) - && StringUtils.isBlank(schemaField.getDescription())) - && curField.getDescription().equals(schemaField.getDescription()); + return StringUtils.equals(curField.getDescription(), schemaField.getDescription()); } private static void processRemoval( @@ -429,6 +396,7 @@ private static void processRemoval( .fieldPath(baseField.getFieldPath()) .fieldUrn(getSchemaFieldUrn(datasetUrn, baseField)) .nullable(baseField.isNullable()) + .modificationCategory(SchemaFieldModificationCategory.OTHER) .auditStamp(auditStamp) .build()); } @@ -467,6 +435,38 @@ private static void processAdd( changeEvents.addAll(propChangeEvents); } + private static void processNativeTypeChange( + ChangeCategory changeCategory, + List changeEvents, + Urn datasetUrn, + SchemaField curBaseField, + SchemaField curTargetField, + AuditStamp auditStamp) { + // Non-backward compatible change + Major version bump + if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + changeEvents.add( + DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() + .category(ChangeCategory.TECHNICAL_SCHEMA) + .modifier(getSchemaFieldUrn(datasetUrn, curBaseField).toString()) + .entityUrn(datasetUrn.toString()) + .operation(ChangeOperation.MODIFY) + .semVerChange(SemanticChangeType.MAJOR) + .description( + String.format( + "%s native datatype of the field '%s' changed from '%s' to '%s'.", + BACKWARDS_INCOMPATIBLE_DESC, + getFieldPathV1(curTargetField), + curBaseField.getNativeDataType(), + curTargetField.getNativeDataType())) + .fieldPath(curBaseField.getFieldPath()) + .fieldUrn(getSchemaFieldUrn(datasetUrn, curBaseField)) + .nullable(curBaseField.isNullable()) + .modificationCategory(SchemaFieldModificationCategory.TYPE_CHANGE) + .auditStamp(auditStamp) + .build()); + } + } + private static ChangeEvent generateRenameEvent( Urn datasetUrn, SchemaField curBaseField, SchemaField curTargetField, AuditStamp auditStamp) { return DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() @@ -505,61 +505,70 @@ private static List getForeignKeyChangeEvents() { } private static List getPrimaryKeyChangeEvents( + ChangeCategory changeCategory, SchemaMetadata baseSchema, SchemaMetadata targetSchema, Urn datasetUrn, AuditStamp auditStamp) { - List primaryKeyChangeEvents = new ArrayList<>(); - Set basePrimaryKeys = - (baseSchema != null && baseSchema.getPrimaryKeys() != null) - ? new HashSet<>(baseSchema.getPrimaryKeys()) - : new HashSet<>(); - Set targetPrimaryKeys = - (targetSchema != null && targetSchema.getPrimaryKeys() != null) - ? new HashSet<>(targetSchema.getPrimaryKeys()) - : new HashSet<>(); - Set removedBaseKeys = - basePrimaryKeys.stream() - .filter(key -> !targetPrimaryKeys.contains(key)) - .collect(Collectors.toSet()); - for (String removedBaseKeyField : removedBaseKeys) { - primaryKeyChangeEvents.add( - ChangeEvent.builder() - .category(ChangeCategory.TECHNICAL_SCHEMA) - .modifier(getSchemaFieldUrn(datasetUrn.toString(), removedBaseKeyField).toString()) - .entityUrn(datasetUrn.toString()) - .operation(ChangeOperation.MODIFY) - .semVerChange(SemanticChangeType.MAJOR) - .description( - BACKWARDS_INCOMPATIBLE_DESC - + " removal of the primary key field '" - + removedBaseKeyField - + "'") - .auditStamp(auditStamp) - .build()); - } - - Set addedTargetKeys = - targetPrimaryKeys.stream() - .filter(key -> !basePrimaryKeys.contains(key)) - .collect(Collectors.toSet()); - for (String addedTargetKeyField : addedTargetKeys) { - primaryKeyChangeEvents.add( - ChangeEvent.builder() - .category(ChangeCategory.TECHNICAL_SCHEMA) - .modifier(getSchemaFieldUrn(datasetUrn, addedTargetKeyField).toString()) - .entityUrn(datasetUrn.toString()) - .operation(ChangeOperation.MODIFY) - .semVerChange(SemanticChangeType.MAJOR) - .description( - BACKWARDS_INCOMPATIBLE_DESC - + " addition of the primary key field '" - + addedTargetKeyField - + "'") - .auditStamp(auditStamp) - .build()); + if (ChangeCategory.TECHNICAL_SCHEMA.equals(changeCategory)) { + List primaryKeyChangeEvents = new ArrayList<>(); + Set basePrimaryKeys = + (baseSchema != null && baseSchema.getPrimaryKeys() != null) + ? new HashSet<>(baseSchema.getPrimaryKeys()) + : new HashSet<>(); + Set targetPrimaryKeys = + (targetSchema != null && targetSchema.getPrimaryKeys() != null) + ? new HashSet<>(targetSchema.getPrimaryKeys()) + : new HashSet<>(); + Set removedBaseKeys = + basePrimaryKeys.stream() + .filter(key -> !targetPrimaryKeys.contains(key)) + .collect(Collectors.toSet()); + + Set addedTargetKeys = + targetPrimaryKeys.stream() + .filter(key -> !basePrimaryKeys.contains(key)) + .collect(Collectors.toSet()); + if (!removedBaseKeys.isEmpty() || !addedTargetKeys.isEmpty()) { + String keyChangeTarget; + // Just pick the first schema field we can find for the change event + if (!removedBaseKeys.isEmpty()) { + keyChangeTarget = removedBaseKeys.stream().findFirst().get(); + } else { + keyChangeTarget = addedTargetKeys.stream().findFirst().get(); + } + + StringBuilder description = + new StringBuilder(BACKWARDS_INCOMPATIBLE_DESC + " a primary key constraint change."); + if (!removedBaseKeys.isEmpty()) { + description.append(" The following fields were removed:"); + removedBaseKeys.forEach( + removedBaseKey -> description.append(" '").append(removedBaseKey).append("'")); + description.append("."); + } + if (!addedTargetKeys.isEmpty()) { + description.append(" The following fields were added:"); + addedTargetKeys.forEach( + addedTargetKey -> description.append(" '").append(addedTargetKey).append("'")); + description.append("."); + } + primaryKeyChangeEvents.add( + DatasetSchemaFieldChangeEvent.schemaFieldChangeEventBuilder() + .category(ChangeCategory.TECHNICAL_SCHEMA) + .fieldUrn(getSchemaFieldUrn(datasetUrn, keyChangeTarget)) + .fieldPath(keyChangeTarget) + .modifier(getSchemaFieldUrn(datasetUrn, keyChangeTarget).toString()) + .entityUrn(datasetUrn.toString()) + .operation(ChangeOperation.MODIFY) + .semVerChange(SemanticChangeType.MAJOR) + .description(description.toString()) + .modificationCategory(SchemaFieldModificationCategory.OTHER) + .auditStamp(auditStamp) + .build()); + return primaryKeyChangeEvents; + } } - return primaryKeyChangeEvents; + return Collections.emptyList(); } @Override diff --git a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java index f90f23cfe5178..d8d33f4c356bb 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/timeline/eventgenerator/SchemaMetadataChangeEventGeneratorTest.java @@ -4,6 +4,7 @@ import com.linkedin.common.AuditStamp; import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.StringArray; import com.linkedin.metadata.timeline.data.ChangeEvent; import com.linkedin.metadata.timeline.data.dataset.SchemaFieldModificationCategory; import com.linkedin.mxe.SystemMetadata; @@ -151,6 +152,64 @@ public void testSchemaFieldRename() throws Exception { Set.of(SchemaFieldModificationCategory.RENAME.toString()), actual); } + @Test + public void testSchemaFieldDropAdd() throws Exception { + // When a rename cannot be detected, treated as drop -> add + SchemaMetadataChangeEventGenerator test = new SchemaMetadataChangeEventGenerator(); + + Urn urn = getTestUrn(); + String entity = "dataset"; + String aspect = "schemaMetadata"; + AuditStamp auditStamp = getTestAuditStamp(); + + Aspect from = + getSchemaMetadata( + List.of(new SchemaField().setFieldPath("ID").setNativeDataType("NUMBER(16,1)"))); + Aspect to3 = + getSchemaMetadata( + List.of(new SchemaField().setFieldPath("ID2").setNativeDataType("NUMBER(10,1)"))); + List actual = test.getChangeEvents(urn, entity, aspect, from, to3, auditStamp); + compareDescriptions( + Set.of( + "A forwards & backwards compatible change due to the newly added field 'ID2'.", + "A backwards incompatible change due to removal of field: 'ID'."), + actual); + assertEquals(2, actual.size()); + compareModificationCategories(Set.of(SchemaFieldModificationCategory.OTHER.toString()), actual); + } + + @Test + public void testSchemaFieldPrimaryKeyChange() throws Exception { + // When a rename cannot be detected, treated as drop -> add + SchemaMetadataChangeEventGenerator test = new SchemaMetadataChangeEventGenerator(); + + Urn urn = getTestUrn(); + String entity = "dataset"; + String aspect = "schemaMetadata"; + AuditStamp auditStamp = getTestAuditStamp(); + + Aspect from = + getSchemaMetadata( + List.of( + new SchemaField().setFieldPath("ID").setNativeDataType("NUMBER(16,1)"), + new SchemaField().setFieldPath("ID2").setNativeDataType("NUMBER(16,1)"))); + from.getValue().setPrimaryKeys(new StringArray(List.of("ID"))); + Aspect to3 = + getSchemaMetadata( + List.of( + new SchemaField().setFieldPath("ID").setNativeDataType("NUMBER(16,1)"), + new SchemaField().setFieldPath("ID2").setNativeDataType("NUMBER(16,1)"))); + to3.getValue().setPrimaryKeys(new StringArray(List.of("ID2"))); + List actual = test.getChangeEvents(urn, entity, aspect, from, to3, auditStamp); + compareDescriptions( + Set.of( + "A backwards incompatible change due to a primary key constraint change. " + + "The following fields were removed: 'ID'. The following fields were added: 'ID2'."), + actual); + assertEquals(1, actual.size()); + compareModificationCategories(Set.of(SchemaFieldModificationCategory.OTHER.toString()), actual); + } + @Test public void testDelete() throws Exception { SchemaMetadataChangeEventGenerator test = new SchemaMetadataChangeEventGenerator(); diff --git a/smoke-test/tests/openapi/v2/timeline.json b/smoke-test/tests/openapi/v2/timeline.json index ccf33ebd9d1c8..5521ee2376278 100644 --- a/smoke-test/tests/openapi/v2/timeline.json +++ b/smoke-test/tests/openapi/v2/timeline.json @@ -338,7 +338,7 @@ ], "json": [ { - "timestamp": 1723245258298, + "timestamp": 1726608877854, "actor": "urn:li:corpuser:__datahub_system", "semVer": "0.0.0-computed", "semVerChange": "MINOR", @@ -431,7 +431,7 @@ "versionStamp": "browsePathsV2:0;dataPlatformInstance:0;datasetKey:0;schemaMetadata:1" }, { - "timestamp": 1723245269788, + "timestamp": 1726608915493, "actor": "urn:li:corpuser:__datahub_system", "semVer": "1.0.0-computed", "semVerChange": "MAJOR", @@ -482,7 +482,7 @@ "versionStamp": "browsePathsV2:0;dataPlatformInstance:0;datasetKey:0;schemaMetadata:2" }, { - "timestamp": 1723245279320, + "timestamp": 1726608930642, "actor": "urn:li:corpuser:__datahub_system", "semVer": "2.0.0-computed", "semVerChange": "MAJOR",