Skip to content

Commit 5254855

Browse files
sonika-shahclaude
andauthored
fix migration: update legacy relatedTerms in glossaryTerm version history after the glossary term realtion changes (#27770)
* fix: strip stale relatedTerms from glossary term version snapshots Extends PR #26586. That fix cleaned glossary_term_entity but not the version snapshots in entity_extension, so GET /versions/{v} still 500s on any pre-1.13 term whose relatedTerms had legacy shape: UnrecognizedPropertyException: Unrecognized field "id" (class TermRelation, has only "term" and "relationType") Predicate matches only legacy snapshots — first item has bare `id` (EntityReference) instead of `term` (TermRelation). Skips correctly- shaped snapshots written on 1.13+. Stripping is safe: relatedTerms is loaded from entity_relationship at read time post-#25886. * v1130: transform legacy relatedTerms in version snapshots instead of stripping Replace the SQL UPDATE that stripped relatedTerms from entity_extension version snapshots with a Java migration that wraps each legacy EntityReference[] item as TermRelation[] (term + relationType="relatedTo"). Version reads deserialize entity_extension JSON directly without rehydrating from entity_relationship, so a strip would lose history per version. The transform preserves it. Designed for tables with millions of rows: keyset paginated by PK (id, extension), batched updates, idempotent on re-run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mysql): remove leftover entity_extension strip in v1130 post-migration The previous edit added the comment pointer above the legacy UPDATE entity_extension SET json = JSON_REMOVE(... '$.relatedTerms') block without removing it. On MySQL that SQL would have stripped relatedTerms from version snapshots BEFORE the Java transform runs, defeating the migration and losing related-term history. Postgres was already correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8fe3014 commit 5254855

5 files changed

Lines changed: 198 additions & 0 deletions

File tree

bootstrap/sql/migrations/native/1.13.0/mysql/postDataMigrationSQLScript.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ UPDATE glossary_term_entity
8080
SET json = JSON_REMOVE(json, '$.relatedTerms')
8181
WHERE JSON_EXTRACT(json, '$.relatedTerms') IS NOT NULL;
8282

83+
-- entity_extension version snapshots: handled by Java migration
84+
-- migrateGlossaryTermVersionRelatedTermsToTermRelation (transforms in place to preserve history).
85+
8386
-- Backfill conceptMappings for existing glossary terms
8487
UPDATE glossary_term_entity
8588
SET json = JSON_SET(COALESCE(json, '{}'), '$.conceptMappings', JSON_ARRAY())

bootstrap/sql/migrations/native/1.13.0/postgres/postDataMigrationSQLScript.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ UPDATE glossary_term_entity
8282
SET json = (json::jsonb - 'relatedTerms')::json
8383
WHERE jsonb_exists(json::jsonb, 'relatedTerms');
8484

85+
-- entity_extension version snapshots: handled by Java migration
86+
-- migrateGlossaryTermVersionRelatedTermsToTermRelation (transforms in place to preserve history).
87+
8588
-- Backfill conceptMappings for existing glossary terms
8689
UPDATE glossary_term_entity
8790
SET json = jsonb_set(COALESCE(json::jsonb, '{}'::jsonb), '{conceptMappings}', '[]'::jsonb)

openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,10 @@ public void runDataMigration() {
2525
+ "Webhook authentication may not work correctly until re-saved.",
2626
e);
2727
}
28+
try {
29+
MigrationUtil.migrateGlossaryTermVersionRelatedTermsToTermRelation(handle);
30+
} catch (Exception e) {
31+
LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e);
32+
}
2833
}
2934
}

openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,10 @@ public void runDataMigration() {
2525
+ "Webhook authentication may not work correctly until re-saved.",
2626
e);
2727
}
28+
try {
29+
MigrationUtil.migrateGlossaryTermVersionRelatedTermsToTermRelation(handle);
30+
} catch (Exception e) {
31+
LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e);
32+
}
2833
}
2934
}

openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1130/MigrationUtil.java

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package org.openmetadata.service.migration.utils.v1130;
22

33
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.ArrayNode;
45
import com.fasterxml.jackson.databind.node.ObjectNode;
56
import java.util.List;
67
import java.util.Map;
78
import lombok.extern.slf4j.Slf4j;
89
import org.jdbi.v3.core.Handle;
10+
import org.jdbi.v3.core.statement.PreparedBatch;
911
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
1012
import org.openmetadata.schema.entity.events.SubscriptionDestination;
1113
import org.openmetadata.schema.utils.JsonUtils;
@@ -129,4 +131,184 @@ public static void migrateWebhookSecretKeyToAuthType(Handle handle) {
129131

130132
LOG.info("Migrated {} event subscriptions with secretKey to authType", migratedCount);
131133
}
134+
135+
private static final String SELECT_GLOSSARY_VERSIONS_MYSQL =
136+
"SELECT id, extension, json FROM entity_extension "
137+
+ "WHERE extension LIKE 'glossaryTerm.version.%' "
138+
+ "AND JSON_CONTAINS_PATH(json, 'one', '$.relatedTerms[0].id') "
139+
+ "AND (id > :id OR (id = :id AND extension > :extension)) "
140+
+ "ORDER BY id, extension LIMIT :pageSize";
141+
142+
private static final String SELECT_GLOSSARY_VERSIONS_POSTGRES =
143+
"SELECT id, extension, json::text AS json FROM entity_extension "
144+
+ "WHERE extension LIKE 'glossaryTerm.version.%' "
145+
+ "AND jsonb_exists((json::jsonb)->'relatedTerms'->0, 'id') "
146+
+ "AND (id > :id OR (id = :id AND extension > :extension)) "
147+
+ "ORDER BY id, extension LIMIT :pageSize";
148+
149+
private static final String UPDATE_VERSION_JSON_MYSQL =
150+
"UPDATE entity_extension SET json = :json WHERE id = :id AND extension = :extension";
151+
152+
private static final String UPDATE_VERSION_JSON_POSTGRES =
153+
"UPDATE entity_extension SET json = :json::jsonb WHERE id = :id AND extension = :extension";
154+
155+
private static final int VERSION_RELATED_TERMS_PAGE_SIZE = 500;
156+
private static final String RELATED_TERMS = "relatedTerms";
157+
private static final String CHANGE_DESCRIPTION = "changeDescription";
158+
159+
/**
160+
* Wraps legacy {@code EntityReference[]} relatedTerms as {@code TermRelation[]} in
161+
* glossaryTerm version snapshots — both top-level and inside changeDescription diff strings.
162+
* Version reads bypass entity_relationship rehydration, so a strip would lose history. Idempotent.
163+
*/
164+
public static void migrateGlossaryTermVersionRelatedTermsToTermRelation(Handle handle) {
165+
LOG.info("v1130: transforming legacy relatedTerms in glossaryTerm version snapshots");
166+
boolean isMySQL = Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL());
167+
String selectSql = isMySQL ? SELECT_GLOSSARY_VERSIONS_MYSQL : SELECT_GLOSSARY_VERSIONS_POSTGRES;
168+
String updateSql = isMySQL ? UPDATE_VERSION_JSON_MYSQL : UPDATE_VERSION_JSON_POSTGRES;
169+
170+
String cursorId = "";
171+
String cursorExtension = "";
172+
long totalTransformed = 0;
173+
long totalSkipped = 0;
174+
int pageNumber = 0;
175+
boolean morePages = true;
176+
177+
while (morePages) {
178+
List<Map<String, Object>> rows =
179+
handle
180+
.createQuery(selectSql)
181+
.bind("id", cursorId)
182+
.bind("extension", cursorExtension)
183+
.bind("pageSize", VERSION_RELATED_TERMS_PAGE_SIZE)
184+
.mapToMap()
185+
.list();
186+
187+
if (rows.isEmpty()) {
188+
break;
189+
}
190+
pageNumber++;
191+
morePages = rows.size() == VERSION_RELATED_TERMS_PAGE_SIZE;
192+
193+
PreparedBatch batch = handle.prepareBatch(updateSql);
194+
int batchedUpdates = 0;
195+
for (Map<String, Object> row : rows) {
196+
String id = String.valueOf(row.get("id"));
197+
String extension = String.valueOf(row.get("extension"));
198+
String jsonStr = String.valueOf(row.get("json"));
199+
200+
cursorId = id;
201+
cursorExtension = extension;
202+
203+
try {
204+
ObjectNode root = (ObjectNode) JsonUtils.readTree(jsonStr);
205+
if (transformSnapshot(root)) {
206+
batch.bind("id", id).bind("extension", extension).bind("json", root.toString()).add();
207+
batchedUpdates++;
208+
}
209+
} catch (Exception e) {
210+
totalSkipped++;
211+
LOG.warn(
212+
"Skipping malformed glossaryTerm version snapshot id={} extension={}: {}",
213+
id,
214+
extension,
215+
e.getMessage());
216+
}
217+
}
218+
219+
if (batchedUpdates > 0) {
220+
batch.execute();
221+
totalTransformed += batchedUpdates;
222+
}
223+
224+
LOG.info(
225+
"v1130 relatedTerms transform: page={} transformed={} skipped={} cursor=({},{})",
226+
pageNumber,
227+
totalTransformed,
228+
totalSkipped,
229+
cursorId,
230+
cursorExtension);
231+
}
232+
233+
LOG.info(
234+
"v1130 relatedTerms transform done: pages={} transformed={} skipped={}",
235+
pageNumber,
236+
totalTransformed,
237+
totalSkipped);
238+
}
239+
240+
private static boolean transformSnapshot(ObjectNode root) {
241+
boolean changed = false;
242+
ArrayNode wrappedTopLevel = wrapLegacyRelatedTerms(root.get(RELATED_TERMS));
243+
if (wrappedTopLevel != null) {
244+
root.set(RELATED_TERMS, wrappedTopLevel);
245+
changed = true;
246+
}
247+
JsonNode changeDescription = root.get(CHANGE_DESCRIPTION);
248+
if (changeDescription instanceof ObjectNode cd) {
249+
changed |= rewriteChangeDescriptionEntries(cd, "fieldsAdded", "newValue");
250+
changed |= rewriteChangeDescriptionEntries(cd, "fieldsDeleted", "oldValue");
251+
changed |= rewriteChangeDescriptionEntries(cd, "fieldsUpdated", "newValue");
252+
changed |= rewriteChangeDescriptionEntries(cd, "fieldsUpdated", "oldValue");
253+
}
254+
return changed;
255+
}
256+
257+
/** Wraps legacy items as TermRelation; returns null when nothing needs wrapping. */
258+
private static ArrayNode wrapLegacyRelatedTerms(JsonNode array) {
259+
if (array == null || !array.isArray() || array.isEmpty()) {
260+
return null;
261+
}
262+
ArrayNode wrapped = JsonUtils.getObjectMapper().createArrayNode();
263+
boolean changed = false;
264+
for (JsonNode item : array) {
265+
if (isWrappedTermRelation(item)) {
266+
wrapped.add(item);
267+
} else {
268+
ObjectNode tr = JsonUtils.getObjectMapper().createObjectNode();
269+
tr.set("term", item);
270+
tr.put("relationType", "relatedTo");
271+
wrapped.add(tr);
272+
changed = true;
273+
}
274+
}
275+
return changed ? wrapped : null;
276+
}
277+
278+
private static boolean isWrappedTermRelation(JsonNode item) {
279+
return item != null && item.isObject() && item.has("term");
280+
}
281+
282+
/** Rewrites legacy relatedTerms items inside changeDescription diff JSON strings. */
283+
private static boolean rewriteChangeDescriptionEntries(
284+
ObjectNode changeDescription, String bucket, String valueField) {
285+
JsonNode entries = changeDescription.get(bucket);
286+
if (entries == null || !entries.isArray()) {
287+
return false;
288+
}
289+
boolean anyChanged = false;
290+
for (JsonNode entry : entries) {
291+
if (!(entry instanceof ObjectNode entryObj)) {
292+
continue;
293+
}
294+
JsonNode nameNode = entryObj.get("name");
295+
if (nameNode == null || !RELATED_TERMS.equals(nameNode.asText())) {
296+
continue;
297+
}
298+
JsonNode valueNode = entryObj.get(valueField);
299+
if (valueNode == null || !valueNode.isTextual() || valueNode.asText().isEmpty()) {
300+
continue;
301+
}
302+
try {
303+
JsonNode parsed = JsonUtils.readTree(valueNode.asText());
304+
ArrayNode wrapped = wrapLegacyRelatedTerms(parsed);
305+
if (wrapped != null) {
306+
entryObj.put(valueField, wrapped.toString());
307+
anyChanged = true;
308+
}
309+
} catch (Exception ignored) {
310+
}
311+
}
312+
return anyChanged;
313+
}
132314
}

0 commit comments

Comments
 (0)