001package gudusoft.gsqlparser.slpc.adapter; 002 003import gudusoft.gsqlparser.dlineage.dataflow.model.ResultSetType; 004import gudusoft.gsqlparser.dlineage.dataflow.model.xml.column; 005import gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow; 006import gudusoft.gsqlparser.dlineage.dataflow.model.xml.argument; 007import gudusoft.gsqlparser.dlineage.dataflow.model.xml.oraclePackage; 008import gudusoft.gsqlparser.dlineage.dataflow.model.xml.procedure; 009import gudusoft.gsqlparser.dlineage.dataflow.model.xml.relationship; 010import gudusoft.gsqlparser.dlineage.dataflow.model.xml.sourceColumn; 011import gudusoft.gsqlparser.dlineage.dataflow.model.xml.table; 012import gudusoft.gsqlparser.dlineage.dataflow.model.xml.targetColumn; 013import gudusoft.gsqlparser.dlineage.dataflow.model.xml.transform; 014import gudusoft.gsqlparser.slpc.identity.SlpcIdentity; 015import gudusoft.gsqlparser.slpc.model.SlpcDocument; 016import gudusoft.gsqlparser.slpc.model.SlpcObject; 017import gudusoft.gsqlparser.slpc.validation.SlpcSemanticValidator; 018 019import java.nio.charset.StandardCharsets; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.security.MessageDigest; 023import java.security.NoSuchAlgorithmException; 024import java.util.ArrayDeque; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.IdentityHashMap; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.TreeMap; 037import java.util.regex.Matcher; 038import java.util.regex.Pattern; 039 040/** 041 * Projects one already-completed legacy {@link dataflow} POJO to SLPC v0.2. 042 * This class never receives or parses SQL and never interprets display text as 043 * identity. Semantics absent from the POJO are quarantined or diagnosed. 044 */ 045public final class LegacyDlineageAdapter { 046 047 public static final String ADAPTER_NAME = "LegacyDlineageAdapter"; 048 public static final String ADAPTER_VERSION = "0.2.0"; 049 private static final String URI_POLICY = "SLPC-URI-OPAQUE-LOCATOR-V1"; 050 private static final Pattern SECRET_LOCATOR = Pattern.compile( 051 "(?i)(?:password|passwd|secret|token|access[_-]?key|credential)\\s*[:=]"); 052 private static final Pattern LEGACY_COORDINATE = Pattern.compile( 053 "\\[\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\]\\s*,\\s*" 054 + "\\[\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\]"); 055 056 public LegacyAdaptation adapt(dataflow legacy, LegacyAdapterContext context) { 057 if (legacy == null) throw new IllegalArgumentException("completed dlineage result must not be null"); 058 if (context == null) throw new IllegalArgumentException("adapter context must not be null"); 059 060 State state = new State(context); 061 state.legacyInputCount = countLegacyRecords(legacy); 062 state.artifact = artifact(context); 063 state.statement = statement(state.artifact, context, legacy); 064 state.statementId = state.statement.string("statementId"); 065 state.hasSourceLocation = state.statement.get("sourceLocation") != null; 066 indexLegacySourceLocations(legacy, state); 067 buildEndpoints(legacy, state); 068 buildRoutineParameterFlows(state); 069 buildObjectRelationships(legacy, state); 070 buildStatementOperation(legacy, state); 071 buildLineage(legacy, state); 072 buildJoinSemantics(legacy, state); 073 buildIntermediateGraph(legacy, state); 074 materializeFactsAndOccurrences(state, state.factOccurrenceDrafts); 075 materializeObjectRelationships(state); 076 accountErrors(legacy, state); 077 return state.finish(); 078 } 079 080 private void buildEndpoints(dataflow legacy, State state) { 081 List<EndpointDraft> drafts = new ArrayList<EndpointDraft>(); 082 List<SystemFieldDraft> systemFields = new ArrayList<SystemFieldDraft>(); 083 for (table relation : dataflow.getAllTables(legacy)) { 084 EndpointDraft relationDraft = endpoint(relation, null, state, true, "OBJECT", relation.getId()); 085 drafts.add(relationDraft); 086 state.tableDrafts.put(relation, relationDraft); 087 for (column field : relation.getColumns()) { 088 if (isRelationRows(field)) { 089 systemFields.add(new SystemFieldDraft(relation, field)); 090 continue; 091 } 092 EndpointDraft fieldDraft = endpoint(relation, field, state, true, "FIELD", field.getId()); 093 drafts.add(fieldDraft); 094 state.columnDrafts.put(field, fieldDraft); 095 } 096 } 097 for (procedure routine : allProcedures(legacy)) { 098 table synthetic = routineTable(routine); 099 LegacyAdapterContext.RoutineBinding binding = state.context.routineBindingResolver().resolve(routine); 100 if (binding != null && !validRoutineBinding(routine, binding, state)) binding = null; 101 if (binding != null && hasReturnParameter(binding)) synthetic.setType("function"); 102 if (binding != null) state.routineBindings.put(routine, binding); 103 EndpointDraft routineDraft = endpoint(synthetic, null, state, false, "PROCEDURE", routine.getId(), 104 binding == null ? null : binding.routineIdentity()); 105 drafts.add(routineDraft); state.routineDrafts.put(routine, routineDraft); 106 if (binding != null) { 107 Map<String, EndpointDraft> parameters = new LinkedHashMap<String, EndpointDraft>(); 108 for (LegacyAdapterContext.RoutineParameterBinding parameter : binding.parameters()) { 109 EndpointDraft parameterDraft = parameterEndpoint(routine, parameter, routineDraft, state); 110 drafts.add(parameterDraft); parameters.put(parameter.bindingKey(), parameterDraft); 111 state.parameterBindingByDraft.put(parameterDraft, parameter); 112 } 113 state.parameterDraftsByRoutine.put(routine, parameters); 114 } 115 } 116 Collections.sort(drafts, new Comparator<EndpointDraft>() { 117 @Override public int compare(EndpointDraft left, EndpointDraft right) { 118 int rank = left.canonical == null ? 1 : 0; 119 int otherRank = right.canonical == null ? 1 : 0; 120 if (rank != otherRank) return rank - otherRank; 121 int key = utf8Compare(left.sortKey, right.sortKey); 122 if (key != 0) return key; 123 return left.stableTieBreaker.compareTo(right.stableTieBreaker); 124 } 125 }); 126 List<EndpointDraft> uniqueDrafts = new ArrayList<EndpointDraft>(); 127 Map<String, EndpointDraft> representativeByKey = new LinkedHashMap<String, EndpointDraft>(); 128 for (EndpointDraft draft : drafts) { 129 String key = (draft.canonical == null ? "1\u0000" : "0\u0000") + draft.sortKey; 130 EndpointDraft representative = representativeByKey.get(key); 131 if (representative == null) { representativeByKey.put(key, draft); uniqueDrafts.add(draft); } 132 else if (!representative.sameSemanticIdentity(draft)) throw new IllegalStateException("SLPC endpoint identity collision: " + draft.sortKey); 133 draft.representative = representative == null ? draft : representative; 134 } 135 for (int index = 0; index < uniqueDrafts.size(); index++) { 136 EndpointDraft draft = uniqueDrafts.get(index); draft.endpointRef = local("ep-", index + 1); 137 SlpcObject endpoint = draft.withRef(); 138 state.endpoints.add(endpoint); 139 state.endpointByRef.put(draft.endpointRef, endpoint); 140 } 141 for (EndpointDraft draft : drafts) { 142 draft.endpointRef = draft.representative.endpointRef; 143 LegacyAdapterContext.RoutineParameterBinding parameter = state.parameterBindingByDraft.get(draft); 144 if (parameter != null) { 145 state.parameterRefByKey.put(parameterKey(draft.routine, parameter.bindingKey()), draft.endpointRef); 146 if (parameter.legacyArgumentId() != null) { 147 state.mappedArgumentIds.add(parameter.legacyArgumentId()); 148 state.coverage.add(new LegacyCoverageReport.Entry("ARGUMENT", parameter.legacyArgumentId(), 149 draft.canonical == null ? LegacyCoverageReport.Outcome.QUARANTINED 150 : LegacyCoverageReport.Outcome.EXACT, 151 draft.endpointRef, draft.canonical == null ? "parameter identity unavailable" 152 : "routine parameter endpoint")); 153 } 154 } else if (draft.field == null) { 155 state.tableRefs.put(draft.relation, draft.endpointRef); 156 putLegacyRef(state.endpointByLegacyId, draft.relation.getId(), draft.endpointRef); 157 state.coverage.add(new LegacyCoverageReport.Entry(draft.legacyKind, safeId(draft.legacyId, draft.stableTieBreaker), 158 draft.canonical == null ? LegacyCoverageReport.Outcome.QUARANTINED : LegacyCoverageReport.Outcome.EXACT, 159 draft.endpointRef, draft.canonical == null ? "identity unavailable" : "canonical endpoint")); 160 } else { 161 state.columnRefs.put(draft.field, draft.endpointRef); 162 putLegacyRef(state.endpointByLegacyId, draft.field.getId(), draft.endpointRef); 163 state.coverage.add(new LegacyCoverageReport.Entry(draft.legacyKind, safeId(draft.legacyId, draft.stableTieBreaker), 164 draft.canonical == null ? LegacyCoverageReport.Outcome.QUARANTINED : LegacyCoverageReport.Outcome.EXACT, 165 draft.endpointRef, draft.canonical == null ? "identity unavailable" : "canonical endpoint")); 166 } 167 } 168 for (Map.Entry<EndpointDraft, LegacyAdapterContext.RoutineParameterBinding> entry 169 : state.parameterBindingByDraft.entrySet()) { 170 EndpointDraft draft = entry.getKey(); 171 LegacyAdapterContext.RoutineParameterBinding parameter = entry.getValue(); 172 if (parameter.legacyArgumentId() != null) { 173 state.endpointByLegacyId.put(parameter.legacyArgumentId(), draft.endpointRef); 174 } 175 if (parameter.boundLegacyEndpointId() != null) { 176 String previous = state.endpointByLegacyId.get(parameter.boundLegacyEndpointId()); 177 if (previous == null) { 178 routineBindingLoss(state, parameter.bindingKey(), 179 "Bound legacy endpoint does not exist in the completed POJO: " 180 + parameter.boundLegacyEndpointId()); 181 } else { 182 state.endpointByLegacyId.put(parameter.boundLegacyEndpointId(), draft.endpointRef); 183 } 184 } 185 } 186 for (SystemFieldDraft systemField : systemFields) { 187 String holderRef = state.tableRefs.get(systemField.relation); 188 putLegacyRef(state.endpointByLegacyId, systemField.field.getId(), holderRef); 189 state.coverage.add(new LegacyCoverageReport.Entry("FIELD", safeId(systemField.field.getId(), "RelationRows"), 190 LegacyCoverageReport.Outcome.CONDITIONAL, holderRef, 191 "SLPC_PSEUDO_COLUMN_REJECTED: normalized to the holder relation")); 192 state.differences.add(new IntentionalDiffLedger.Entry( 193 IntentionalDiffLedger.Category.EXPECTED_SEMANTIC_CORRECTION, 194 safeId(systemField.field.getId(), "RelationRows"), "SLPC_PSEUDO_COLUMN_REJECTED", 195 "Legacy RelationRows is normalized to its holder relation and is not published as a field endpoint.")); 196 } 197 if (!systemFields.isEmpty()) { 198 state.diagnostics.add(diagnostic("SLPC_PSEUDO_COLUMN_REJECTED", "INFO", "DOCUMENT", null, 199 "Legacy RelationRows system columns were normalized to holder relations instead of field endpoints.")); 200 } 201 if (hasUnavailableIdentity(drafts)) { 202 state.diagnostics.add(diagnostic("SLPC_IDENTITY_UNAVAILABLE", "INFO", "DOCUMENT", null, 203 "Legacy display names are not treated as persistent identity; unresolved endpoints remain candidates.")); 204 } 205 if (state.hasLegacyEndpointAmbiguity) { 206 state.diagnostics.add(diagnostic("SLPC_ENDPOINT_AMBIGUOUS", "WARN", "DOCUMENT", null, 207 "At least one unresolved legacy relation carries multiple candidate tables; no candidate name was promoted to identity.")); 208 } 209 if (state.hasStorageIdentityUnavailable) { 210 state.diagnostics.add(diagnostic("SLPC_STORAGE_IDENTITY_UNAVAILABLE", "WARN", "DOCUMENT", null, 211 "At least one storage locator was absent, malformed, credential-bearing, or contained query/fragment data; it was redacted and quarantined.")); 212 } 213 accountNonEndpointObjects(legacy, state); 214 } 215 216 private EndpointDraft endpoint(table relation, column field, State state, boolean allowIdentity, 217 String legacyKind, String legacyId) { 218 return endpoint(relation, field, state, allowIdentity, legacyKind, legacyId, null); 219 } 220 221 private EndpointDraft endpoint(table relation, column field, State state, boolean allowIdentity, 222 String legacyKind, String legacyId, LegacyAdapterContext.ResolvedIdentity resolvedOverride) { 223 String objectKind = "PROCEDURE".equals(legacyKind) ? routineObjectKind(relation) : objectKind(relation, state); 224 String level = field == null ? endpointLevel(relation, legacyKind) : "FIELD"; 225 String storageLocator = field == null && "FILE".equals(objectKind) ? normalizedStorageLocator(relation) : null; 226 List<Object> display = storageLocator == null ? displaySegments(relation, field) 227 : Collections.<Object>singletonList(display("OBJECT", storageLocator)); 228 if (field == null && "FILE".equals(objectKind) && storageLocator == null) { 229 state.hasStorageIdentityUnavailable = true; 230 display = Collections.<Object>singletonList(display("OBJECT", "<redacted-storage-locator>")); 231 } 232 boolean intermediate = !"PROCEDURE".equals(legacyKind) && isIntermediate(relation); 233 LegacyAdapterContext.ResolvedIdentity resolved = resolvedOverride != null ? resolvedOverride 234 : intermediate || !allowIdentity || storageLocator != null || isScopedObjectKind(objectKind) ? null 235 : state.context.identityResolver().resolve(relation, field); 236 SlpcObject identity; 237 String resolution, publication, canonical = null; 238 List<Object> extensions = Collections.emptyList(); 239 if (storageLocator != null) { 240 String scheme = storageScheme(storageLocator); 241 identity = SlpcObject.builder().put("kind", "URI_EXACT").put("strength", "EXACT") 242 .put("normalizationPolicyId", URI_POLICY).put("scheme", scheme) 243 .put("locatorDigest", uriLocatorDigest(URI_POLICY, scheme, storageLocator)).build(); 244 resolution = "RESOLVED"; publication = "ELIGIBLE"; 245 extensions = Collections.<Object>singletonList(SlpcObject.builder() 246 .put("extensionType", "gsp.storage-asset.v1").put("schemaVersion", "1.0.0") 247 .put("criticality", "OPTIONAL").put("payload", SlpcObject.builder() 248 .put("secretFreeNormalizedLocator", storageLocator).build()).build()); 249 } else if (resolved == null) { 250 boolean ambiguous = hasCandidateTables(relation); 251 if (ambiguous) state.hasLegacyEndpointAmbiguity = true; 252 String reason = intermediate ? "UNBOUND_RESULT_SET" : ambiguous ? "AMBIGUOUS" 253 : isScopedObjectKind(objectKind) ? "SCOPE_UNAVAILABLE" : "OTHER"; 254 identity = SlpcObject.builder().put("kind", "UNAVAILABLE").put("strength", "UNAVAILABLE").put("reason", reason).build(); 255 resolution = ambiguous ? "AMBIGUOUS" : "UNRESOLVED"; publication = "INELIGIBLE"; 256 } else { 257 identity = resolved.identity(); resolution = resolved.resolutionStatus(); publication = resolved.publicationState(); 258 } 259 SlpcObject provisional = SlpcObject.builder().put("endpointRef", "ep-000001").putNull("canonicalEndpointId") 260 .put("endpointLevel", level).put("objectKind", objectKind).put("displaySegments", display) 261 .put("identity", identity).put("resolutionStatus", resolution).put("publicationState", publication) 262 .putNull("scope").put("extensions", extensions).build(); 263 if ((resolved != null || storageLocator != null) && isCanonicalIdentity(identity)) { 264 canonical = SlpcIdentity.endpointId(provisional); 265 provisional = provisional.toBuilder().put("canonicalEndpointId", canonical).build(); 266 } 267 String sortKey = canonical == null ? SlpcIdentity.candidateEndpointKey(provisional) : canonical; 268 String tie = safe(relation.getId()) + "\u0000" + safe(field == null ? null : field.getId()) + "\u0000" 269 + safe(relation.getName()) + "\u0000" + safe(field == null ? null : field.getName()); 270 return new EndpointDraft(relation, field, provisional, canonical, sortKey, tie, legacyKind, legacyId); 271 } 272 273 private String normalizedStorageLocator(table relation) { 274 String raw = nonEmpty(relation.getUri()); 275 if (raw == null) raw = nonEmpty(relation.getLocation()); 276 if (raw == null) raw = nonEmpty(relation.getName()); 277 if (raw == null) return null; 278 raw = raw.trim(); 279 if (raw.length() >= 2 && ((raw.charAt(0) == '\'' && raw.charAt(raw.length() - 1) == '\'') 280 || (raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"'))) { 281 raw = raw.substring(1, raw.length() - 1); 282 } 283 if (SECRET_LOCATOR.matcher(raw).find()) return null; 284 try { 285 URI uri = new URI(raw).normalize(); 286 if (uri.getScheme() == null || uri.getScheme().isEmpty() || uri.getUserInfo() != null 287 || uri.getQuery() != null || uri.getFragment() != null) return null; 288 String scheme = uri.getScheme().toLowerCase(java.util.Locale.ROOT); 289 String ascii = uri.toASCIIString(); 290 return scheme + ascii.substring(uri.getScheme().length()); 291 } catch (URISyntaxException invalid) { 292 return null; 293 } 294 } 295 296 private String storageScheme(String locator) { 297 int separator = locator.indexOf(':'); 298 if (separator <= 0) throw new IllegalArgumentException("normalized storage locator has no scheme"); 299 return locator.substring(0, separator); 300 } 301 302 private String uriLocatorDigest(String policy, String scheme, String locator) { 303 try { 304 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 305 digest.update("SLPC-URI-LOCATOR-DIGEST-V1".getBytes(StandardCharsets.US_ASCII)); 306 digest.update((byte) 0); digest.update(SlpcIdentity.string(policy)); 307 digest.update(SlpcIdentity.string(scheme)); digest.update(SlpcIdentity.string(locator)); 308 StringBuilder result = new StringBuilder(); 309 for (byte item : digest.digest()) result.append(String.format("%02x", item & 255)); 310 return result.toString(); 311 } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); } 312 } 313 314 private List<procedure> allProcedures(dataflow legacy) { 315 List<procedure> result = new ArrayList<procedure>(legacy.getProcedures()); 316 for (oraclePackage pkg : legacy.getPackages()) result.addAll(pkg.getProcedures()); 317 return result; 318 } 319 320 private table routineTable(procedure routine) { 321 table result = new table(); result.setId(routine.getId()); result.setName(safe(routine.getName())); 322 result.setServer(routine.getServer()); result.setDatabase(routine.getDatabase()); result.setSchema(routine.getSchema()); 323 result.setType("function".equalsIgnoreCase(routine.getType()) ? "function" : "procedure"); 324 return result; 325 } 326 327 private boolean validRoutineBinding(procedure routine, LegacyAdapterContext.RoutineBinding binding, State state) { 328 Set<String> argumentIds = new HashSet<String>(); 329 for (argument item : routine.getArguments()) if (item.getId() != null) argumentIds.add(item.getId()); 330 Set<String> usedArguments = new HashSet<String>(), keys = new HashSet<String>(); 331 Set<Integer> ordinals = new HashSet<Integer>(); 332 boolean valid = true; 333 for (LegacyAdapterContext.RoutineParameterBinding parameter : binding.parameters()) { 334 if (!keys.add(parameter.bindingKey()) || !ordinals.add(parameter.ordinal()) 335 || !equalsAny(parameter.role(), "IN", "OUT", "INOUT", "RETURN", "TABLE_RETURN_FIELD")) { 336 valid = false; break; 337 } 338 if (parameter.legacyArgumentId() == null) { 339 if (!equalsAny(parameter.role(), "RETURN", "TABLE_RETURN_FIELD")) { valid = false; break; } 340 } else if (!argumentIds.contains(parameter.legacyArgumentId()) 341 || !usedArguments.add(parameter.legacyArgumentId())) { 342 valid = false; break; 343 } 344 } 345 if (!valid) routineBindingLoss(state, safeId(routine.getId(), safe(routine.getName())), 346 "Routine binding contains an unknown/duplicate argument, ordinal, binding key, or parameter role."); 347 return valid; 348 } 349 350 private boolean hasReturnParameter(LegacyAdapterContext.RoutineBinding binding) { 351 for (LegacyAdapterContext.RoutineParameterBinding parameter : binding.parameters()) { 352 if (equalsAny(parameter.role(), "RETURN", "TABLE_RETURN_FIELD")) return true; 353 } 354 return false; 355 } 356 357 private EndpointDraft parameterEndpoint(procedure routine, 358 LegacyAdapterContext.RoutineParameterBinding parameter, EndpointDraft routineDraft, State state) { 359 table synthetic = routineTable(routine); 360 column field = new column(); field.setId(parameter.legacyArgumentId() == null 361 ? "return:" + parameter.bindingKey() : parameter.legacyArgumentId()); 362 field.setName(parameter.displayName()); 363 LegacyAdapterContext.ResolvedIdentity resolved = parameter.identity(); 364 SlpcObject identity = resolved.identity(); String canonical = null; 365 String resolution = resolved.resolutionStatus(), publication = resolved.publicationState(); 366 SlpcObject scope = null; 367 if (routineDraft.canonical != null) { 368 scope = SlpcObject.builder().put("scopeKind", "PROCEDURE").put("scopeId", routineDraft.canonical) 369 .put("definedAtStatementId", state.statementId).build(); 370 } else { 371 identity = SlpcObject.builder().put("kind", "UNAVAILABLE").put("strength", "UNAVAILABLE") 372 .put("reason", "SCOPE_UNAVAILABLE").build(); 373 resolution = "UNRESOLVED"; publication = "INELIGIBLE"; 374 } 375 SlpcObject.Builder payload = SlpcObject.builder().put("parameterRole", parameter.role()) 376 .put("parameterOrdinal", parameter.ordinal()).put("routineBindingKey", parameter.bindingKey()); 377 if (parameter.declaredType() != null) payload.put("declaredType", parameter.declaredType()); 378 List<Object> extensions = Collections.<Object>singletonList(SlpcObject.builder() 379 .put("extensionType", "gsp.routine-parameter.v1").put("schemaVersion", "1.0.0") 380 .put("criticality", "OPTIONAL").put("payload", payload.build()).build()); 381 SlpcObject provisional = SlpcObject.builder().put("endpointRef", "ep-000001").putNull("canonicalEndpointId") 382 .put("endpointLevel", "VARIABLE").put("objectKind", "PARAMETER") 383 .put("displaySegments", displaySegments(synthetic, field)).put("identity", identity) 384 .put("resolutionStatus", resolution).put("publicationState", publication) 385 .put("scope", scope).put("extensions", extensions).build(); 386 if (routineDraft.canonical != null && isCanonicalIdentity(identity)) { 387 canonical = SlpcIdentity.endpointId(provisional); 388 provisional = provisional.toBuilder().put("canonicalEndpointId", canonical).build(); 389 } 390 String sortKey = canonical == null ? SlpcIdentity.candidateEndpointKey(provisional) : canonical; 391 String tie = safe(routine.getId()) + "\u0000parameter\u0000" + parameter.ordinal() 392 + "\u0000" + parameter.bindingKey(); 393 EndpointDraft result = new EndpointDraft(synthetic, field, provisional, canonical, sortKey, tie, 394 parameter.legacyArgumentId() == null ? "RETURN_PARAMETER" : "ARGUMENT", 395 parameter.legacyArgumentId() == null ? parameter.bindingKey() : parameter.legacyArgumentId()); 396 result.routine = routine; 397 return result; 398 } 399 400 private String parameterKey(procedure routine, String bindingKey) { 401 return safeId(routine.getId(), safe(routine.getName())) + "\u0000" + bindingKey; 402 } 403 404 private void routineBindingLoss(State state, String owner, String message) { 405 state.diagnostics.add(diagnostic("SLPC_ROUTINE_BINDING_UNPROVEN", "WARN", "DOCUMENT", null, message)); 406 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 407 owner, "GSP_LEGACY_ROUTINE_ARGUMENT_BINDING_UNAVAILABLE", message)); 408 } 409 410 private void accountNonEndpointObjects(dataflow legacy, State state) { 411 for (oraclePackage pkg : legacy.getPackages()) { 412 String owner = safeId(pkg.getId(), safe(pkg.getName())); 413 state.coverage.add(new LegacyCoverageReport.Entry("PACKAGE", owner, 414 LegacyCoverageReport.Outcome.UNSUPPORTED_WITH_LOSS, null, "GSP_LEGACY_PACKAGE_ENDPOINT_UNSUPPORTED")); 415 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 416 owner, "GSP_LEGACY_PACKAGE_ENDPOINT_UNSUPPORTED", "SLPC v0.2 has no package endpoint kind; contained routines remain visible.")); 417 for (argument item : pkg.getArguments()) accountArgument(item, state); 418 for (procedure routine : pkg.getProcedures()) for (argument item : routine.getArguments()) accountArgument(item, state); 419 } 420 for (procedure routine : legacy.getProcedures()) for (argument item : routine.getArguments()) accountArgument(item, state); 421 for (gudusoft.gsqlparser.dlineage.dataflow.model.xml.process process : legacy.getProcesses()) { 422 String owner = safeId(process.getId(), "process"); 423 state.coverage.add(new LegacyCoverageReport.Entry("PROCESS", owner, 424 LegacyCoverageReport.Outcome.CONDITIONAL, state.statementId, 425 "Legacy process is represented by artifact/statement provenance, not a catalog endpoint.")); 426 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.EXPECTED_PRESENTATION_DIFFERENCE, 427 owner, "GSP_LEGACY_PROCESS_AS_PROVENANCE", "Legacy process presentation maps to SLPC artifact/statement provenance.")); 428 } 429 } 430 431 private void accountArgument(argument item, State state) { 432 if (item.getId() != null && state.mappedArgumentIds.contains(item.getId())) return; 433 String owner = safeId(item.getId(), "argument"); 434 state.diagnostics.add(diagnostic("SLPC_ROUTINE_BINDING_UNPROVEN", "WARN", "DOCUMENT", null, 435 "A routine argument was retained only in loss accounting because no authoritative overload/formal binding was supplied.")); 436 state.coverage.add(new LegacyCoverageReport.Entry("ARGUMENT", owner, 437 LegacyCoverageReport.Outcome.UNSUPPORTED_WITH_LOSS, null, "GSP_LEGACY_ROUTINE_ARGUMENT_BINDING_UNAVAILABLE")); 438 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 439 owner, "GSP_LEGACY_ROUTINE_ARGUMENT_BINDING_UNAVAILABLE", "An argument needs a proven routine/parameter binding before it can become an endpoint or fact.")); 440 } 441 442 private void buildRoutineParameterFlows(State state) { 443 for (Map.Entry<procedure, LegacyAdapterContext.RoutineBinding> entry : state.routineBindings.entrySet()) { 444 procedure routine = entry.getKey(); LegacyAdapterContext.RoutineBinding binding = entry.getValue(); 445 String routineRef = endpointAtLevel(state.endpointByLegacyId.get(routine.getId()), "ROUTINE", state); 446 if (routineRef == null || !canonicalEndpoint(routineRef, state)) { 447 routineBindingLoss(state, safeId(routine.getId(), safe(routine.getName())), 448 "Routine parameters require a canonical overload-aware routine identity."); 449 continue; 450 } 451 state.hasRoutineBinding = true; 452 for (LegacyAdapterContext.RoutineValueFlow flow : binding.flows()) { 453 if (!equalsAny(flow.role(), "DIRECT", "TRANSFORM", "CONDITION", "AGGREGATE", "DERIVED") 454 || !equalsAny(flow.operationKind(), "NONE", "GROUP_BY", "WINDOW", "SORT") 455 || !equalsAny(flow.operationInputKind(), "NONE", "GROUP_KEY", "PARTITION_KEY", 456 "ORDER_KEY", "FRAME_BOUND")) { 457 routineBindingLoss(state, safeId(routine.getId(), safe(routine.getName())), 458 "Routine flow returned an axis/role/operation code outside the SLPC v0.2 registry."); 459 continue; 460 } 461 String sourceRef = routineValueRef(routine, flow.source(), state); 462 String targetRef = routineValueRef(routine, flow.target(), state); 463 if (sourceRef == null || targetRef == null || sourceRef.equals(targetRef)) { 464 routineBindingLoss(state, safeId(routine.getId(), safe(routine.getName())), 465 "Routine flow did not bind two distinct completed-POJO/parameter endpoints."); 466 continue; 467 } 468 FactSemantics semantics = FactSemantics.publish("VALUE", flow.role(), flow.operationKind(), 469 flow.operationInputKind(), false); 470 SlpcObject semantic = semanticRecord(sourceRef, targetRef, semantics, state); 471 if (state.parameterRefByKey.containsValue(sourceRef) 472 || state.parameterRefByKey.containsValue(targetRef)) state.hasRoutineParameterFlow = true; 473 boolean canonical = isCanonicalFact(semantic, state.endpointByRef); 474 String key = canonical ? SlpcIdentity.factId(semantic, state.endpointByRef) 475 : SlpcIdentity.factCandidateFingerprint(semantic, state.endpointByRef); 476 FactGroup group = state.factGroups.get(key); 477 if (group == null) { 478 group = new FactGroup(semantic, canonical, key); state.factGroups.put(key, group); 479 } else if (!group.sameSemantic(semantic)) { 480 throw new IllegalStateException("SLPC routine fact fingerprint collision: " + key); 481 } 482 FactOccurrenceDraft occurrence = new FactOccurrenceDraft(group, null, null, 483 canonical ? "RESOLVED" : "UNRESOLVED", Collections.<Object>emptyList(), 484 Collections.<Object>emptyList(), sourceLocations(routine.getCoordinate(), state), routineRef); 485 group.occurrences.add(occurrence); state.factOccurrenceDrafts.add(occurrence); 486 state.hasRoutineParameterFlow = true; 487 } 488 } 489 } 490 491 private String routineValueRef(procedure routine, LegacyAdapterContext.RoutineValueRef value, State state) { 492 String ref = value.parameterBindingKey() == null 493 ? state.endpointByLegacyId.get(value.legacyEndpointId()) 494 : state.parameterRefByKey.get(parameterKey(routine, value.parameterBindingKey())); 495 SlpcObject endpoint = ref == null ? null : state.endpointByRef.get(ref); 496 if (endpoint == null || !("FIELD".equals(endpoint.string("endpointLevel")) 497 || "VARIABLE".equals(endpoint.string("endpointLevel")))) return null; 498 return ref; 499 } 500 501 private void buildObjectRelationships(dataflow legacy, State state) { 502 for (table relation : dataflow.getAllTables(legacy)) { 503 String relationRef = state.tableRefs.get(relation); 504 if (relationRef == null || isIntermediate(relation)) continue; 505 List<String> primary = new ArrayList<String>(); 506 for (column field : relation.getColumns()) { 507 String fieldRef = state.columnRefs.get(field); 508 if (fieldRef == null) continue; 509 if (Boolean.TRUE.equals(field.isPrimaryKey())) primary.add(fieldRef); 510 if (Boolean.TRUE.equals(field.isUnqiueKey())) { 511 addKeyRelationship("UNIQUE_KEY", relationRef, 512 Collections.singletonList(fieldRef), "column:" + safeId(field.getId(), field.getName()), state); 513 } 514 } 515 if (!primary.isEmpty()) addKeyRelationship("PRIMARY_KEY", relationRef, primary, 516 "table:" + safeId(relation.getId(), relation.getName()), state); 517 } 518 519 for (relationship relation : legacy.getRelationships()) { 520 String type = safe(relation.getType()); 521 if ("call".equals(type)) { 522 mapCallRelationship(relation, state); 523 } else if ("fdd".equals(type)) { 524 mapNonFactObjectRelationship(relation, state); 525 } 526 } 527 mapForeignKeyRelationships(legacy, state); 528 } 529 530 private void addKeyRelationship(String kind, String relationRef, List<String> fields, 531 String legacyEvidence, State state) { 532 sortEndpointRefs(fields, state); 533 List<Object> participants = Collections.<Object>singletonList( 534 SlpcObject.builder().put("role", "KEY_RELATION").put("endpointRef", relationRef).build()); 535 List<Object> keyFields = new ArrayList<Object>(); 536 for (int index = 0; index < fields.size(); index++) { 537 keyFields.add(SlpcObject.builder().put("ordinal", index + 1) 538 .put("fieldEndpointRef", fields.get(index)).build()); 539 } 540 addObjectRelationship(kind, participants, keyFields, null, legacyEvidence, state); 541 } 542 543 private void mapForeignKeyRelationships(dataflow legacy, State state) { 544 Map<String, relationship> byId = new HashMap<String, relationship>(); 545 List<relationship> foreignKeys = new ArrayList<relationship>(); 546 for (relationship relation : legacy.getRelationships()) { 547 if (!("er".equals(relation.getType()) 548 || "fdd".equals(relation.getType()) && "foreign_key".equals(relation.getEffectType()))) continue; 549 foreignKeys.add(relation); byId.put(legacyId(relation), relation); 550 } 551 for (LegacyAdapterContext.ForeignKeyBinding binding 552 : state.context.compositeForeignKeyResolver().resolve(legacy)) { 553 if (!mapAuthoritativeForeignKey(binding, byId, state)) { 554 for (String evidenceId : binding.evidenceRelationshipIds()) { 555 relationship relation = byId.get(evidenceId); 556 if (relation != null) accountDedicatedLoss(relation, state, "SLPC_COMPOSITE_FK_PAIRING_UNPROVEN", 557 "Caller-supplied composite FK pairing failed validation against the completed POJO."); 558 } 559 } 560 } 561 Map<String, List<relationship>> unresolvedFamilies = new LinkedHashMap<String, List<relationship>>(); 562 for (relationship relation : foreignKeys) { 563 if (state.dedicatedRelationshipAccounting.contains(legacyId(relation))) continue; 564 String family = foreignKeyFamily(relation, state); 565 if (!unresolvedFamilies.containsKey(family)) unresolvedFamilies.put(family, new ArrayList<relationship>()); 566 unresolvedFamilies.get(family).add(relation); 567 } 568 for (List<relationship> family : unresolvedFamilies.values()) { 569 Set<String> foreignFields = new HashSet<String>(), referencedFields = new HashSet<String>(); 570 for (relationship relation : family) { 571 String target = relation.getTarget() == null ? null : resolveTargetField(relation.getTarget(), state); 572 if (target != null) foreignFields.add(target); 573 for (sourceColumn source : relation.getSources()) { 574 String ref = resolveSourceField(source, state); if (ref != null) referencedFields.add(ref); 575 } 576 } 577 if (foreignFields.size() == 1 && referencedFields.size() == 1) { 578 for (relationship relation : family) mapSingleColumnForeignKey(relation, state); 579 } else { 580 for (relationship relation : family) accountDedicatedLoss(relation, state, 581 "SLPC_COMPOSITE_FK_PAIRING_UNPROVEN", 582 "Legacy FK/ER output does not retain declaration-order composite key pairs; authoritative AST/catalog pairing is required."); 583 } 584 } 585 } 586 587 private String foreignKeyFamily(relationship relation, State state) { 588 String foreign = relationEndpoint(relation.getTarget(), state), referenced = null; 589 for (sourceColumn source : relation.getSources()) { 590 String candidate = relationEndpoint(source, state); 591 if (referenced == null) referenced = candidate; 592 else if (!equalValues(referenced, candidate)) return "invalid:" + legacyId(relation); 593 } 594 return "fk:" + safe(foreign) + "\u0000" + safe(referenced); 595 } 596 597 private boolean mapAuthoritativeForeignKey(LegacyAdapterContext.ForeignKeyBinding binding, 598 Map<String, relationship> byId, State state) { 599 Set<String> evidenceIds = new LinkedHashSet<String>(binding.evidenceRelationshipIds()); 600 if (evidenceIds.size() != binding.evidenceRelationshipIds().size()) return false; 601 List<relationship> evidence = new ArrayList<relationship>(); 602 for (String id : evidenceIds) { 603 relationship relation = byId.get(id); if (relation == null) return false; evidence.add(relation); 604 } 605 String foreignRelation = null, referencedRelation = null; 606 Set<String> foreignFields = new HashSet<String>(), referencedFields = new HashSet<String>(); 607 List<Object> pairs = new ArrayList<Object>(); 608 int ordinal = 1; 609 for (LegacyAdapterContext.ForeignKeyPair pair : binding.keyPairs()) { 610 String foreignField = endpointAtLevel(state.endpointByLegacyId.get(pair.foreignKeyFieldLegacyId()), "FIELD", state); 611 String referencedField = endpointAtLevel(state.endpointByLegacyId.get(pair.referencedFieldLegacyId()), "FIELD", state); 612 if (foreignField == null || referencedField == null 613 || !foreignFields.add(foreignField) || !referencedFields.add(referencedField)) return false; 614 String pairForeignRelation = null, pairReferencedRelation = null; 615 for (relationship relation : evidence) { 616 if (matches(relation.getTarget(), pair.foreignKeyFieldLegacyId())) { 617 pairForeignRelation = relationEndpoint(relation.getTarget(), state); 618 for (sourceColumn source : relation.getSources()) { 619 if (matches(source, pair.referencedFieldLegacyId())) { 620 pairReferencedRelation = relationEndpoint(source, state); break; 621 } 622 } 623 } 624 if (pairForeignRelation != null && pairReferencedRelation != null) break; 625 } 626 if (pairForeignRelation == null || pairReferencedRelation == null) return false; 627 if (foreignRelation == null) foreignRelation = pairForeignRelation; 628 if (referencedRelation == null) referencedRelation = pairReferencedRelation; 629 if (!foreignRelation.equals(pairForeignRelation) || !referencedRelation.equals(pairReferencedRelation)) return false; 630 pairs.add(SlpcObject.builder().put("ordinal", ordinal++) 631 .put("foreignKeyFieldRef", foreignField).put("referencedFieldRef", referencedField).build()); 632 } 633 if (foreignRelation == null || referencedRelation == null) return false; 634 List<Object> participants = new ArrayList<Object>(); 635 participants.add(participant("FOREIGN_KEY_RELATION", foreignRelation)); 636 participants.add(participant("REFERENCED_RELATION", referencedRelation)); 637 for (relationship relation : evidence) { 638 addObjectRelationship("FOREIGN_KEY_REFERENCE", participants, null, pairs, legacyId(relation), state); 639 markForeignKeyMapped(relation, state); 640 } 641 state.diagnostics.add(diagnostic("SLPC_COMPOSITE_FK_PAIRING_PROVEN", "INFO", "DOCUMENT", null, 642 "Composite foreign-key keyPairs preserve caller-authoritative declaration order.")); 643 return true; 644 } 645 646 private boolean matches(targetColumn value, String legacyFieldId) { 647 return value != null && (legacyFieldId.equals(value.getId()) || legacyFieldId.equals(value.getTarget_id())); 648 } 649 650 private boolean matches(sourceColumn value, String legacyFieldId) { 651 return value != null && (legacyFieldId.equals(value.getId()) || legacyFieldId.equals(value.getSource_id())); 652 } 653 654 private void mapSingleColumnForeignKey(relationship relation, State state) { 655 targetColumn target = relation.getTarget(); 656 if (target == null || relation.getSources().isEmpty()) { 657 accountDedicatedLoss(relation, state, "SLPC_OBJECT_RELATIONSHIP_UNPUBLISHABLE", 658 "Legacy FK/ER record has no target/source key pair."); 659 return; 660 } 661 String foreignRelation = relationEndpoint(target, state); 662 String foreignField = resolveTargetField(target, state); 663 sourceColumn source = relation.getSources().get(0); 664 String referencedRelation = relationEndpoint(source, state); 665 String referencedField = resolveSourceField(source, state); 666 boolean mapped = foreignRelation != null && foreignField != null 667 && referencedRelation != null && referencedField != null; 668 if (mapped) { 669 List<Object> participants = new ArrayList<Object>(); 670 participants.add(participant("FOREIGN_KEY_RELATION", foreignRelation)); 671 participants.add(participant("REFERENCED_RELATION", referencedRelation)); 672 List<Object> pairs = Collections.<Object>singletonList(SlpcObject.builder().put("ordinal", 1) 673 .put("foreignKeyFieldRef", foreignField).put("referencedFieldRef", referencedField).build()); 674 addObjectRelationship("FOREIGN_KEY_REFERENCE", participants, null, pairs, legacyId(relation), state); 675 } 676 if (mapped) { 677 markForeignKeyMapped(relation, state); 678 } else { 679 accountDedicatedLoss(relation, state, "SLPC_OBJECT_RELATIONSHIP_UNPUBLISHABLE", 680 "Legacy FK/ER participants could not be bound at relation and field levels."); 681 } 682 } 683 684 private void markForeignKeyMapped(relationship relation, State state) { 685 markDedicatedMapped(relation, state, "FOREIGN_KEY_REFERENCE"); 686 state.diagnostics.add(diagnostic("SLPC_LEGACY_FK_DATAFLOW_RECLASSIFIED", "INFO", "DOCUMENT", null, 687 "Legacy FK/ER dataflow was reclassified as a structural FOREIGN_KEY_REFERENCE.")); 688 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.EXPECTED_SEMANTIC_CORRECTION, 689 legacyId(relation), "SLPC_LEGACY_FK_DATAFLOW_RECLASSIFIED", 690 "A catalog foreign key is structural metadata and is not a runtime VALUE dependency.")); 691 } 692 693 private void mapCallRelationship(relationship relation, State state) { 694 String caller = routineEndpoint(relation.getCaller(), relation.getProcedureId(), state); 695 boolean mapped = false; 696 boolean hasCallee = false; 697 for (sourceColumn calleeValue : relation.getCallees()) { 698 String callee = routineEndpoint(calleeValue, state); 699 if (callee != null) hasCallee = true; 700 if (caller == null || callee == null) continue; 701 List<Object> participants = new ArrayList<Object>(); 702 participants.add(SlpcObject.builder().put("role", "CALLER_ROUTINE").put("endpointRef", caller).build()); 703 participants.add(SlpcObject.builder().put("role", "CALLEE_ROUTINE").put("endpointRef", callee).build()); 704 addObjectRelationship("CALLS", participants, null, null, legacyId(relation), state); 705 mapped = true; 706 } 707 if (mapped) markDedicatedMapped(relation, state, "CALLS"); 708 else if (caller == null && hasCallee) { 709 // A top-level CALL has no caller routine by definition. It is 710 // accounted by the CALL_ROUTINE statement operation builder. 711 } 712 else accountDedicatedLoss(relation, state, "SLPC_OBJECT_RELATIONSHIP_UNPUBLISHABLE", 713 "A CALLS relationship requires both a proven caller routine and a cataloged callee routine; top-level calls remain statement operations."); 714 } 715 716 private void mapNonFactObjectRelationship(relationship relation, State state) { 717 targetColumn target = relation.getTarget(); 718 if (target == null) return; 719 String targetRelation = relationEndpoint(target, state); 720 for (sourceColumn source : relation.getSources()) { 721 String sourceRelation = relationEndpoint(source, state); 722 if (targetRelation == null || sourceRelation == null) continue; 723 String targetKind = endpointKind(targetRelation, state); 724 String sourceKind = endpointKind(sourceRelation, state); 725 List<Object> participants = new ArrayList<Object>(); 726 String relationshipKind = null; 727 if ("STREAM".equals(targetKind) && !isStorageKind(sourceKind)) { 728 relationshipKind = "STREAMS_FROM"; 729 participants.add(participant("STREAM", targetRelation)); 730 participants.add(participant("BASE_RELATION", sourceRelation)); 731 } else if (isStorageKind(sourceKind) && !isStorageKind(targetKind)) { 732 relationshipKind = "READS_FROM"; 733 participants.add(participant("CONSUMER_RELATION", targetRelation)); 734 participants.add(participant("STORAGE_ASSET", sourceRelation)); 735 } else if (!isStorageKind(sourceKind) && isStorageKind(targetKind)) { 736 relationshipKind = "WRITES_TO"; 737 participants.add(participant("PRODUCER_RELATION", sourceRelation)); 738 participants.add(participant("STORAGE_ASSET", targetRelation)); 739 } else if (equalsAny(relation.getEffectType(), "synonym", "create_synonym")) { 740 relationshipKind = "SYNONYM_OF"; 741 participants.add(participant("SYNONYM", targetRelation)); 742 participants.add(participant("BASE_OBJECT", sourceRelation)); 743 } 744 if (relationshipKind != null) addObjectRelationship(relationshipKind, participants, null, null, 745 legacyId(relation), state); 746 } 747 } 748 749 private SlpcObject participant(String role, String endpointRef) { 750 return SlpcObject.builder().put("role", role).put("endpointRef", endpointRef).build(); 751 } 752 753 private boolean isStorageKind(String kind) { return "FILE".equals(kind) || "STAGE".equals(kind); } 754 755 private boolean isScopedObjectKind(String kind) { 756 return equalsAny(kind, "TEMP_TABLE", "GLOBAL_TEMP_TABLE", "TABLE_VARIABLE", "SCALAR_VARIABLE", "CURSOR", "PARAMETER"); 757 } 758 759 private String endpointKind(String ref, State state) { 760 SlpcObject endpoint = ref == null ? null : state.endpointByRef.get(ref); 761 return endpoint == null ? null : endpoint.string("objectKind"); 762 } 763 764 private String routineEndpoint(targetColumn value, String fallbackId, State state) { 765 if (value == null) return endpointAtLevel(state.endpointByLegacyId.get(fallbackId), "ROUTINE", state); 766 String result = endpointAtLevel(state.endpointByLegacyId.get(value.getId()), "ROUTINE", state); 767 if (result == null) result = endpointAtLevel(state.endpointByLegacyId.get(value.getTarget_id()), "ROUTINE", state); 768 if (result == null) result = endpointAtLevel(state.endpointByLegacyId.get(value.getParent_id()), "ROUTINE", state); 769 if (result == null) result = endpointAtLevel(state.endpointByLegacyId.get(fallbackId), "ROUTINE", state); 770 return result; 771 } 772 773 private String routineEndpoint(sourceColumn value, State state) { 774 String result = endpointAtLevel(state.endpointByLegacyId.get(value.getId()), "ROUTINE", state); 775 if (result == null) result = endpointAtLevel(state.endpointByLegacyId.get(value.getSource_id()), "ROUTINE", state); 776 if (result == null) result = endpointAtLevel(state.endpointByLegacyId.get(value.getParent_id()), "ROUTINE", state); 777 return result; 778 } 779 780 private String routineEndpoint(String legacyProcedureId, State state) { 781 return endpointAtLevel(state.endpointByLegacyId.get(legacyProcedureId), "ROUTINE", state); 782 } 783 784 private void addObjectRelationship(String kind, List<Object> participants, List<Object> keyFields, 785 List<Object> keyPairs, String legacyEvidence, State state) { 786 SlpcObject.Builder semanticBuilder = SlpcObject.builder().put("relationshipKind", kind) 787 .put("participants", participants); 788 if (keyFields != null) semanticBuilder.put("keyFields", keyFields); 789 if (keyPairs != null) semanticBuilder.put("keyPairs", keyPairs); 790 SlpcObject semantic = semanticBuilder.build(); 791 boolean canonical = relationshipCanonical(semantic, state); 792 String fingerprint = canonical ? SlpcIdentity.objectRelationshipId(semantic, state.endpointByRef) 793 : SlpcIdentity.objectRelationshipCandidateFingerprint(semantic, state.endpointByRef); 794 RelationshipGroup group = state.relationshipGroups.get(fingerprint); 795 if (group == null) { 796 group = new RelationshipGroup(semantic, canonical, fingerprint); 797 state.relationshipGroups.put(fingerprint, group); 798 } else if (!group.semantic.equals(semantic)) { 799 throw new IllegalStateException("SLPC object relationship fingerprint collision: " + fingerprint); 800 } 801 group.legacyEvidence.add(legacyEvidence); 802 List<Object> locations = state.sourceLocationsByLegacyEvidence.get(legacyEvidence); 803 if (locations != null) for (Object value : locations) { 804 SlpcObject location = (SlpcObject) value; 805 group.sourceLocations.put(sourceLocationKey(location), location); 806 } 807 } 808 809 private boolean relationshipCanonical(SlpcObject semantic, State state) { 810 for (Object value : semantic.array("participants")) { 811 if (!canonicalEndpoint(((SlpcObject) value).string("endpointRef"), state)) return false; 812 } 813 for (String details : new String[] {"keyFields", "keyPairs"}) { 814 List<Object> values = semantic.array(details); 815 if (values == null) continue; 816 for (Object value : values) { 817 SlpcObject item = (SlpcObject) value; 818 for (String field : new String[] {"fieldEndpointRef", "foreignKeyFieldRef", "referencedFieldRef"}) { 819 if (item.string(field) != null && !canonicalEndpoint(item.string(field), state)) return false; 820 } 821 } 822 } 823 return true; 824 } 825 826 private boolean canonicalEndpoint(String ref, State state) { 827 SlpcObject endpoint = ref == null ? null : state.endpointByRef.get(ref); 828 return endpoint != null && endpoint.string("canonicalEndpointId") != null; 829 } 830 831 private void markDedicatedMapped(relationship relation, State state, String target) { 832 if (!state.dedicatedRelationshipAccounting.add(legacyId(relation))) return; 833 state.coverage.add(new LegacyCoverageReport.Entry("RELATIONSHIP", legacyId(relation), 834 LegacyCoverageReport.Outcome.CONDITIONAL, target, 835 "legacy record mapped to typed SLPC structural/operation semantics; identity controls publication")); 836 } 837 838 private void accountDedicatedLoss(relationship relation, State state, String code, String message) { 839 if (!state.dedicatedRelationshipAccounting.add(legacyId(relation))) return; 840 accountLoss(relation, state, code, message); 841 } 842 843 private void buildLineage(dataflow legacy, State state) { 844 for (relationship relation : legacy.getRelationships()) { 845 if (equalsAny(relation.getType(), "join", "call", "er", "crud", "fddi")) { 846 // Consumed by their dedicated structural, operation, join, or 847 // verified path-evidence mapping. 848 continue; 849 } 850 if (!"fdd".equals(relation.getType()) && !"fdr".equals(relation.getType()) 851 && !"frd".equals(relation.getType())) { 852 accountUnsupportedRelationship(relation, state); 853 continue; 854 } 855 if (state.dedicatedRelationshipAccounting.contains(legacyId(relation))) continue; 856 if (unsupportedFactEffect(relation.getEffectType())) { 857 accountLoss(relation, state, "SLPC_OPERATION_UNSUPPORTED", 858 "Legacy effectType '" + safe(relation.getEffectType()) 859 + "' requires a typed operation/object relationship mapping and is not published as a VALUE/ROW fact in M1."); 860 continue; 861 } 862 targetColumn target = relation.getTarget(); 863 if (target == null || relation.getSources().isEmpty()) { 864 accountLoss(relation, state, "SLPC_ENDPOINT_UNRESOLVED", 865 "Dataflow relation has no target or source in the completed legacy POJO."); 866 continue; 867 } 868 List<String> owners = new ArrayList<String>(); 869 boolean conditional = effectNeedsAdditionalMapping(relation.getEffectType()); 870 if (conditional) { 871 accountSemanticGap(relation, "SLPC_OPERATION_UNSUPPORTED", 872 "Legacy effectType '" + safe(relation.getEffectType()) 873 + "' retains its proven data dependency, but its additional statement/operation semantics are not mapped in M1.", state); 874 } 875 for (sourceColumn source : relation.getSources()) { 876 if (hasCandidateParents(source)) { 877 conditional = true; 878 accountSemanticGap(relation, "SLPC_ENDPOINT_AMBIGUOUS", 879 "Legacy sourceColumn contains multiple candidate parents; the adapter will not select one as the lineage source.", state); 880 continue; 881 } 882 FactSemantics semantics = semantics(relation, source); 883 if (semantics.diagnosticCode != null) { 884 conditional = true; 885 accountSemanticGap(relation, semantics.diagnosticCode, semantics.diagnosticMessage, state); 886 } 887 if (!semantics.publish) continue; 888 String targetRef = resolveTarget(target, semantics, state); 889 if (targetRef == null) { 890 accountSemanticGap(relation, "SLPC_ENDPOINT_UNRESOLVED", 891 "The legacy target cannot be bound at the endpoint level required by " + semantics.axis + ".", state); 892 continue; 893 } 894 String sourceRef = semantics.derived ? null : resolveSource(source, semantics, state); 895 if (!semantics.derived && sourceRef == null) { 896 accountSemanticGap(relation, "SLPC_ENDPOINT_UNRESOLVED", 897 "A proven legacy relation source could not be bound without coarsening its endpoint level.", state); 898 continue; 899 } 900 SlpcObject semantic = semanticRecord(sourceRef, targetRef, semantics, state); 901 boolean canonical = isCanonicalFact(semantic, state.endpointByRef); 902 String semanticKey = canonical ? SlpcIdentity.factId(semantic, state.endpointByRef) 903 : SlpcIdentity.factCandidateFingerprint(semantic, state.endpointByRef); 904 FactGroup group = state.factGroups.get(semanticKey); 905 if (group == null) { 906 group = new FactGroup(semantic, canonical, semanticKey); 907 state.factGroups.put(semanticKey, group); 908 } else if (!group.sameSemantic(semantic)) { 909 throw new IllegalStateException("SLPC fact fingerprint collision: " + semanticKey); 910 } 911 SlpcObject transformation = transformationEvidence(relation, source); 912 SlpcObject predicate = evidence(predicateText(relation, semantics)); 913 List<Object> literalInputs = semantics.derived 914 ? Collections.<Object>singletonList(redactedLiteral(state, state.nextRedactionOrdinal++)) 915 : Collections.<Object>emptyList(); 916 FactOccurrenceDraft occurrence = new FactOccurrenceDraft(group, transformation, predicate, 917 canonical ? "RESOLVED" : "UNRESOLVED", literalInputs, Collections.<Object>emptyList(), 918 sourceLocations(relation, state), routineEndpoint(relation.getProcedureId(), state)); 919 group.occurrences.add(occurrence); state.factOccurrenceDrafts.add(occurrence); owners.add(semanticKey); 920 } 921 if (owners.isEmpty()) { 922 accountLoss(relation, state, "SLPC_ENDPOINT_NOT_PUBLISHABLE", 923 "No source could be represented without inventing an endpoint."); 924 } else { 925 state.coverage.add(new LegacyCoverageReport.Entry("RELATIONSHIP", legacyId(relation), 926 conditional ? LegacyCoverageReport.Outcome.CONDITIONAL 927 : allCanonical(owners, state) ? LegacyCoverageReport.Outcome.EXACT : LegacyCoverageReport.Outcome.QUARANTINED, 928 join(owners), "VALUE/ROW semantics preserved; identity controls publication")); 929 } 930 } 931 } 932 933 private void buildStatementOperation(dataflow legacy, State state) { 934 String kind = operationKind(state.context.statementKind()); 935 List<Object> participants = new ArrayList<Object>(); 936 List<String> physicalSources = new ArrayList<String>(), targets = new ArrayList<String>(), outputs = new ArrayList<String>(); 937 for (table relation : dataflow.getAllTables(legacy)) { 938 String ref = state.tableRefs.get(relation); 939 if (ref == null) continue; 940 if (isIntermediate(relation)) outputs.add(ref); 941 else if (relation.isTarget() || createdInSql(relation)) targets.add(ref); 942 else physicalSources.add(ref); 943 } 944 sortEndpointRefs(physicalSources, state); sortEndpointRefs(targets, state); sortEndpointRefs(outputs, state); 945 physicalSources = distinct(physicalSources); targets = distinct(targets); outputs = distinct(outputs); 946 if ("QUERY".equals(kind)) { 947 addParticipants(participants, "READ_RELATION", physicalSources); 948 if (!outputs.isEmpty()) { 949 operationLoss(state, "SLPC_ADAPTER_CAPABILITY_GAP", 950 "Legacy result-set names and cleared isTarget flags do not prove which intermediate is the top-level QUERY output; OUTPUT_RELATION is omitted."); 951 } 952 } else if ("INSERT".equals(kind) || "UPDATE".equals(kind) || "DELETE".equals(kind)) { 953 List<String> writeTargets = physicalRelationshipTargets(legacy, state); 954 if (writeTargets.size() != 1) { 955 operationLoss(state, "SLPC_ENDPOINT_UNRESOLVED", 956 "A " + kind + " operation requires exactly one proven legacy target; observed " + writeTargets.size() + "."); 957 return; 958 } 959 addParticipants(participants, "WRITE_TARGET", writeTargets); 960 physicalSources.removeAll(writeTargets); 961 addParticipants(participants, "READ_SOURCE", physicalSources); 962 } else if ("MERGE".equals(kind)) { 963 List<String> mergeTargets = physicalRelationshipTargets(legacy, state); 964 if (mergeTargets.size() != 1) { 965 operationLoss(state, "SLPC_ENDPOINT_UNRESOLVED", 966 "A MERGE operation requires exactly one proven legacy target; observed " + mergeTargets.size() + "."); 967 return; 968 } 969 addParticipants(participants, "MERGE_TARGET", mergeTargets); 970 physicalSources.removeAll(mergeTargets); 971 addParticipants(participants, "MERGE_SOURCE", physicalSources); 972 } else if (equalsAny(kind, "CREATE_TABLE", "CREATE_TABLE_AS", "CREATE_VIEW", "CREATE_MATERIALIZED_VIEW")) { 973 List<String> createdObjects = "CREATE_VIEW".equals(kind) ? refsByKind(legacy, state, "VIEW") 974 : "CREATE_MATERIALIZED_VIEW".equals(kind) ? refsByKind(legacy, state, "MATERIALIZED_VIEW") 975 : targets; 976 String created = singleOrLoss(createdObjects, kind + " CREATED_OBJECT", state); 977 if (created == null) return; 978 addParticipants(participants, "CREATED_OBJECT", Collections.singletonList(created)); 979 if (!"CREATE_TABLE".equals(kind)) addParticipants(participants, "READ_SOURCE", physicalSources); 980 } else if ("CREATE_EXTERNAL_TABLE".equals(kind)) { 981 List<String> storage = refsByStorage(legacy, state, true), relations = refsByStorage(legacy, state, false); 982 relations.retainAll(targets); 983 String created = singleOrLoss(relations, "CREATE_EXTERNAL_TABLE CREATED_OBJECT", state); 984 if (created == null) return; 985 addParticipants(participants, "CREATED_OBJECT", Collections.singletonList(created)); 986 addParticipants(participants, "STORAGE_SOURCE", storage); 987 } else if ("CREATE_STAGE".equals(kind)) { 988 List<String> stages = refsByKind(legacy, state, "STAGE"); 989 String created = singleOrLoss(stages, "CREATE_STAGE CREATED_OBJECT", state); 990 if (created == null) return; 991 addParticipants(participants, "CREATED_OBJECT", Collections.singletonList(created)); 992 List<String> files = refsByKind(legacy, state, "FILE"); 993 addParticipants(participants, "STORAGE_SOURCE", files); 994 } else if ("CREATE_STREAM".equals(kind)) { 995 String[] pair = firstRelationshipPair(state.relationshipGroups, "STREAMS_FROM"); 996 if (pair == null || !canonicalEndpoint(pair[0], state) || !canonicalEndpoint(pair[1], state)) { 997 operationLoss(state, "SLPC_ENDPOINT_UNRESOLVED", 998 "CREATE_STREAM requires one canonical STREAMS_FROM pair; candidate-only structure is retained without an invalid operation."); 999 return; 1000 } 1001 addParticipants(participants, "CREATED_STREAM", Collections.singletonList(pair[0])); 1002 addParticipants(participants, "BASE_RELATION", Collections.singletonList(pair[1])); 1003 } else if ("CREATE_ROUTINE".equals(kind)) { 1004 String routine = singleOrLoss(refsAtLevel(state, "ROUTINE"), "CREATE_ROUTINE CREATED_ROUTINE", state); 1005 if (routine == null) return; 1006 addParticipants(participants, "CREATED_ROUTINE", Collections.singletonList(routine)); 1007 } else if ("CALL_ROUTINE".equals(kind)) { 1008 List<String> callees = new ArrayList<String>(); 1009 for (relationship relation : legacy.getRelationships()) if ("call".equals(relation.getType())) { 1010 for (sourceColumn callee : relation.getCallees()) { 1011 String ref = routineEndpoint(callee, state); if (ref != null) callees.add(ref); 1012 } 1013 } 1014 sortEndpointRefs(callees, state); callees = distinct(callees); 1015 String callee = singleOrLoss(callees, "CALL_ROUTINE CALLEE_ROUTINE", state); 1016 if (callee == null) return; 1017 addParticipants(participants, "CALLEE_ROUTINE", Collections.singletonList(callee)); 1018 for (relationship relation : legacy.getRelationships()) if ("call".equals(relation.getType())) { 1019 markDedicatedMapped(relation, state, "CALL_ROUTINE"); 1020 } 1021 } else if ("RENAME".equals(kind) || "SWAP".equals(kind) || "CLONE".equals(kind)) { 1022 String effect = "RENAME".equals(kind) ? "rename_table" : "SWAP".equals(kind) ? "swap_table" : "clone_table"; 1023 String[] pair = operationPair(legacy, effect, state); 1024 if (pair == null) { operationLoss(state, "SLPC_ENDPOINT_UNRESOLVED", kind + " requires one proven source/target object pair."); return; } 1025 if ("RENAME".equals(kind)) { 1026 addParticipants(participants, "BEFORE_OBJECT", Collections.singletonList(pair[0])); 1027 addParticipants(participants, "AFTER_OBJECT", Collections.singletonList(pair[1])); 1028 } else if ("SWAP".equals(kind)) { 1029 addParticipants(participants, "SWAP_LEFT_OBJECT", Collections.singletonList(pair[1])); 1030 addParticipants(participants, "SWAP_RIGHT_OBJECT", Collections.singletonList(pair[0])); 1031 } else { 1032 addParticipants(participants, "CLONE_SOURCE", Collections.singletonList(pair[0])); 1033 addParticipants(participants, "CLONE_TARGET", Collections.singletonList(pair[1])); 1034 } 1035 } else if ("LOAD".equals(kind)) { 1036 List<String> storage = refsByStorage(legacy, state, true); 1037 List<String> writes = nonStorageTargets(legacy, state); 1038 String target = singleOrLoss(writes, "LOAD WRITE_TARGET", state); if (target == null) return; 1039 addParticipants(participants, "STORAGE_SOURCE", storage); 1040 addParticipants(participants, "WRITE_TARGET", Collections.singletonList(target)); 1041 markEffects(legacy, state, "LOAD", "copy", "load"); 1042 } else if ("UNLOAD".equals(kind) || "INSERT_OVERWRITE_DIRECTORY".equals(kind)) { 1043 List<String> storage = refsByStorage(legacy, state, true); 1044 String target = singleOrLoss(storage, kind + " STORAGE_TARGET", state); if (target == null) return; 1045 addParticipants(participants, "READ_SOURCE", nonStorageSources(legacy, state)); 1046 addParticipants(participants, "STORAGE_TARGET", Collections.singletonList(target)); 1047 markEffects(legacy, state, kind, "unload", "copy"); 1048 } else if (equalsAny(kind, "ALTER", "DROP", "TRUNCATE")) { 1049 List<String> affected = affectedObjects(legacy, state); 1050 String target = singleOrLoss(affected, kind + " AFFECTED_OBJECT", state); if (target == null) return; 1051 addParticipants(participants, "AFFECTED_OBJECT", Collections.singletonList(target)); 1052 for (relationship relation : legacy.getRelationships()) if ("crud".equals(relation.getType()) 1053 || isOperationEffect(relation.getEffectType())) markDedicatedMapped(relation, state, kind); 1054 } else { 1055 List<String> affected = new ArrayList<String>(); affected.addAll(targets); affected.addAll(physicalSources); 1056 sortEndpointRefs(affected, state); addParticipants(participants, "AFFECTED_OBJECT", distinct(affected)); 1057 } 1058 String content = contentSemantics(kind); 1059 String resolution = participantsCanonical(participants, state) ? "RESOLVED" : "UNRESOLVED"; 1060 SlpcObject provisional = SlpcObject.builder().put("statementOperationId", "slso1-" + zeros(64)) 1061 .put("statementId", state.statementId).put("operationOrdinal", 1).put("operationKind", kind) 1062 .put("participants", participants).put("contentSemantics", content).put("quality", occurrenceQuality(resolution)) 1063 .put("sourceLocations", statementSourceLocations(state.statement)) 1064 .put("extensions", Collections.emptyList()).build(); 1065 state.statementOperations.add(provisional.toBuilder() 1066 .put("statementOperationId", SlpcIdentity.statementOperationId(provisional, state.endpointByRef)).build()); 1067 } 1068 1069 private void operationLoss(State state, String code, String message) { 1070 state.diagnostics.add(diagnostic(code, "WARN", "STATEMENT", state.statementId, message)); 1071 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 1072 state.statementId, code, message)); 1073 } 1074 1075 private String operationKind(String statementKind) { 1076 String normalized = safe(statementKind).trim().toUpperCase(java.util.Locale.ROOT) 1077 .replace('-', '_').replace(' ', '_'); 1078 if ("SELECT".equals(normalized)) return "QUERY"; 1079 if ("CREATE_PROCEDURE".equals(normalized) || "CREATE_FUNCTION".equals(normalized)) return "CREATE_ROUTINE"; 1080 if ("CALL".equals(normalized) || "EXECUTE".equals(normalized) || "EXEC".equals(normalized)) return "CALL_ROUTINE"; 1081 if ("ALTER_TABLE".equals(normalized)) return "ALTER"; 1082 if ("DROP_TABLE".equals(normalized)) return "DROP"; 1083 if ("TRUNCATE_TABLE".equals(normalized)) return "TRUNCATE"; 1084 if (equalsAny(normalized, "QUERY", "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE_TABLE", 1085 "CREATE_TABLE_AS", "CREATE_VIEW", "CREATE_MATERIALIZED_VIEW", "CREATE_EXTERNAL_TABLE", 1086 "CREATE_STAGE", "CREATE_STREAM", "CREATE_ROUTINE", "CREATE_SYNONYM", "CREATE_INDEX", 1087 "RENAME", "SWAP", "CLONE", "LOAD", "UNLOAD", "INSERT_OVERWRITE_DIRECTORY", 1088 "CALL_ROUTINE", "ALTER", "DROP", "TRUNCATE", "OTHER")) return normalized; 1089 return "OTHER"; 1090 } 1091 1092 private String contentSemantics(String operationKind) { 1093 if ("QUERY".equals(operationKind)) return "CONTENT_READ"; 1094 if ("INSERT".equals(operationKind) || "LOAD".equals(operationKind)) return "CONTENT_WRITE"; 1095 if (equalsAny(operationKind, "UPDATE", "DELETE", "MERGE", "ALTER")) return "MUTATION"; 1096 if (equalsAny(operationKind, "CREATE_TABLE", "CREATE_VIEW", "CREATE_EXTERNAL_TABLE", "CREATE_STAGE", 1097 "CREATE_STREAM", "CREATE_ROUTINE", "CREATE_SYNONYM", "CREATE_INDEX")) return "DEFINITION_ONLY"; 1098 if (equalsAny(operationKind, "CREATE_TABLE_AS", "CREATE_MATERIALIZED_VIEW", "CLONE", "UNLOAD", 1099 "INSERT_OVERWRITE_DIRECTORY")) return "CONTENT_COPY"; 1100 if ("RENAME".equals(operationKind)) return "IDENTITY_CONTINUITY"; 1101 if ("SWAP".equals(operationKind)) return "CONTENT_EXCHANGE"; 1102 if ("CALL_ROUTINE".equals(operationKind)) return "NOT_APPLICABLE"; 1103 if (equalsAny(operationKind, "DROP", "TRUNCATE")) return "DELETION"; 1104 return "UNKNOWN"; 1105 } 1106 1107 private String singleOrLoss(List<String> values, String label, State state) { 1108 sortEndpointRefs(values, state); values = distinct(values); 1109 if (values.size() == 1) return values.get(0); 1110 operationLoss(state, "SLPC_ENDPOINT_UNRESOLVED", label + " requires exactly one endpoint; observed " + values.size() + "."); 1111 return null; 1112 } 1113 1114 private List<String> refsAtLevel(State state, String level) { 1115 List<String> result = new ArrayList<String>(); 1116 for (Map.Entry<String, SlpcObject> entry : state.endpointByRef.entrySet()) { 1117 if (level.equals(entry.getValue().string("endpointLevel"))) result.add(entry.getKey()); 1118 } 1119 sortEndpointRefs(result, state); return distinct(result); 1120 } 1121 1122 private List<String> refsByKind(dataflow legacy, State state, String kind) { 1123 List<String> result = new ArrayList<String>(); 1124 for (table relation : dataflow.getAllTables(legacy)) { 1125 String ref = state.tableRefs.get(relation); 1126 if (ref != null && kind.equals(endpointKind(ref, state))) result.add(ref); 1127 } 1128 sortEndpointRefs(result, state); return distinct(result); 1129 } 1130 1131 private List<String> refsByStorage(dataflow legacy, State state, boolean storage) { 1132 List<String> result = new ArrayList<String>(); 1133 for (table relation : dataflow.getAllTables(legacy)) { 1134 String ref = state.tableRefs.get(relation); if (ref == null || isIntermediate(relation)) continue; 1135 if (isStorageKind(endpointKind(ref, state)) == storage) result.add(ref); 1136 } 1137 sortEndpointRefs(result, state); return distinct(result); 1138 } 1139 1140 private List<String> nonStorageTargets(dataflow legacy, State state) { 1141 List<String> result = new ArrayList<String>(); 1142 for (relationship relation : legacy.getRelationships()) { 1143 if (relation.getTarget() == null) continue; 1144 String ref = relationEndpoint(relation.getTarget(), state); 1145 if (ref != null && !isStorageKind(endpointKind(ref, state))) result.add(ref); 1146 } 1147 sortEndpointRefs(result, state); return distinct(result); 1148 } 1149 1150 private List<String> physicalRelationshipTargets(dataflow legacy, State state) { 1151 List<String> physical = new ArrayList<String>(); 1152 for (relationship relation : legacy.getRelationships()) { 1153 if (!"fdd".equals(relation.getType()) || "foreign_key".equals(relation.getEffectType()) 1154 || relation.getTarget() == null) continue; 1155 String ref = relationEndpoint(relation.getTarget(), state); 1156 if (ref != null && !isStorageKind(endpointKind(ref, state)) 1157 && !isIntermediateEndpoint(ref, state)) physical.add(ref); 1158 } 1159 sortEndpointRefs(physical, state); return distinct(physical); 1160 } 1161 1162 private List<String> nonStorageSources(dataflow legacy, State state) { 1163 List<String> result = new ArrayList<String>(); 1164 for (relationship relation : legacy.getRelationships()) for (sourceColumn source : relation.getSources()) { 1165 String ref = relationEndpoint(source, state); 1166 if (ref != null && !isStorageKind(endpointKind(ref, state)) && !isIntermediateEndpoint(ref, state)) result.add(ref); 1167 } 1168 sortEndpointRefs(result, state); return distinct(result); 1169 } 1170 1171 private boolean isIntermediateEndpoint(String ref, State state) { 1172 String kind = endpointKind(ref, state); 1173 return equalsAny(kind, "RESULT_SET", "CTE", "DERIVED_TABLE"); 1174 } 1175 1176 private String[] operationPair(dataflow legacy, String effect, State state) { 1177 for (relationship relation : legacy.getRelationships()) { 1178 if (!effect.equals(relation.getEffectType()) || relation.getTarget() == null || relation.getSources().size() != 1) continue; 1179 String before = relationEndpoint(relation.getSources().get(0), state); 1180 String after = relationEndpoint(relation.getTarget(), state); 1181 if (before == null || after == null) continue; 1182 markDedicatedMapped(relation, state, effect); 1183 return new String[] {before, after}; 1184 } 1185 return null; 1186 } 1187 1188 private String[] firstRelationshipPair(Map<String, RelationshipGroup> groups, String kind) { 1189 for (RelationshipGroup group : groups.values()) { 1190 if (!kind.equals(group.semantic.string("relationshipKind"))) continue; 1191 List<Object> participants = group.semantic.array("participants"); 1192 if (participants.size() == 2) return new String[] { 1193 ((SlpcObject) participants.get(0)).string("endpointRef"), 1194 ((SlpcObject) participants.get(1)).string("endpointRef")}; 1195 } 1196 return null; 1197 } 1198 1199 private List<String> affectedObjects(dataflow legacy, State state) { 1200 List<String> result = new ArrayList<String>(); 1201 for (relationship relation : legacy.getRelationships()) { 1202 if (!"crud".equals(relation.getType()) && !isOperationEffect(relation.getEffectType())) continue; 1203 if (relation.getTarget() != null) { 1204 String ref = relationEndpoint(relation.getTarget(), state); 1205 if (ref == null) ref = endpointAtLevel(state.endpointByLegacyId.get(relation.getTarget().getId()), "RELATION", state); 1206 if (ref == null) ref = endpointAtLevel(state.endpointByLegacyId.get(relation.getTarget().getTarget_id()), "RELATION", state); 1207 if (ref != null) result.add(ref); 1208 } 1209 for (sourceColumn source : relation.getSources()) { 1210 String ref = relationEndpoint(source, state); if (ref != null) result.add(ref); 1211 } 1212 } 1213 if (result.isEmpty()) { 1214 for (table relation : dataflow.getAllTables(legacy)) if (relation.isTarget() && !isIntermediate(relation)) { 1215 String ref = state.tableRefs.get(relation); if (ref != null) result.add(ref); 1216 } 1217 } 1218 sortEndpointRefs(result, state); return distinct(result); 1219 } 1220 1221 private boolean isOperationEffect(String effect) { 1222 return equalsAny(effect, "rename_table", "swap_table", "clone_table", "clone_schema", "clone_database", 1223 "unload", "copy", "drop_table", "drop_table_column", "add_table_column", "truncate_table"); 1224 } 1225 1226 private boolean createdInSql(table relation) { 1227 return "true".equalsIgnoreCase(nonEmpty(relation.getCreatedInSql())); 1228 } 1229 1230 private void markEffects(dataflow legacy, State state, String target, String... effects) { 1231 for (relationship relation : legacy.getRelationships()) { 1232 if (equalsAny(relation.getEffectType(), effects)) markDedicatedMapped(relation, state, target); 1233 } 1234 } 1235 1236 private void addParticipants(List<Object> target, String role, List<String> endpointRefs) { 1237 for (String ref : endpointRefs) target.add(SlpcObject.builder().put("role", role).put("endpointRef", ref).build()); 1238 } 1239 1240 private void sortEndpointRefs(List<String> refs, final State state) { 1241 Collections.sort(refs, new Comparator<String>() { @Override public int compare(String left, String right) { 1242 return utf8Compare(SlpcIdentity.candidateEndpointKey(state.endpointByRef.get(left)), 1243 SlpcIdentity.candidateEndpointKey(state.endpointByRef.get(right))); 1244 }}); 1245 } 1246 1247 private List<String> distinct(List<String> values) { 1248 List<String> result = new ArrayList<String>(); 1249 for (String value : values) if (!result.contains(value)) result.add(value); 1250 return result; 1251 } 1252 1253 private boolean participantsCanonical(List<Object> participants, State state) { 1254 for (Object value : participants) { 1255 SlpcObject participant = (SlpcObject) value; 1256 if (state.endpointByRef.get(participant.string("endpointRef")).string("canonicalEndpointId") == null) return false; 1257 } 1258 return true; 1259 } 1260 1261 private void materializeFactsAndOccurrences(State state, List<FactOccurrenceDraft> occurrenceDrafts) { 1262 List<FactGroup> canonical = new ArrayList<FactGroup>(), candidates = new ArrayList<FactGroup>(); 1263 for (FactGroup group : state.factGroups.values()) { if (group.canonical) canonical.add(group); else candidates.add(group); } 1264 Collections.sort(canonical, factGroupComparator()); Collections.sort(candidates, factGroupComparator()); 1265 for (FactGroup group : canonical) { group.ownerKind = "FACT"; group.ownerId = group.semanticKey; } 1266 for (int index = 0; index < candidates.size(); index++) { FactGroup group = candidates.get(index); group.ownerKind = "FACT_CANDIDATE"; group.ownerId = local("fact-cand-", index + 1); } 1267 1268 Map<String, String> ownerSemanticKeys = new HashMap<String, String>(); 1269 for (FactGroup group : canonical) ownerSemanticKeys.put(group.ownerKind + "\u0000" + group.ownerId, group.semanticKey); 1270 for (FactGroup group : candidates) ownerSemanticKeys.put(group.ownerKind + "\u0000" + group.ownerId, group.semanticKey); 1271 Map<String, SlpcObject> uniqueOccurrences = new TreeMap<String, SlpcObject>(); 1272 for (FactOccurrenceDraft draft : occurrenceDrafts) { 1273 SlpcObject withoutId = occurrence(draft, state, "slocc1-" + zeros(64)); 1274 String id = SlpcIdentity.occurrenceId(withoutId, ownerSemanticKeys); 1275 SlpcObject occurrence = withoutId.toBuilder().put("occurrenceId", id).build(); 1276 SlpcObject previous = uniqueOccurrences.get(id); 1277 uniqueOccurrences.put(id, previous == null ? occurrence : mergeOccurrence(previous, occurrence)); 1278 } 1279 state.occurrences.addAll(uniqueOccurrences.values()); 1280 1281 Map<String, Integer> evidenceCounts = new HashMap<String, Integer>(); 1282 for (Object value : state.occurrences) { 1283 SlpcObject occurrence = (SlpcObject) value; 1284 SlpcObject owner = occurrence.object("ownerRef"); String key = owner.string("kind") + "\u0000" + owner.string("id"); 1285 Integer old = evidenceCounts.get(key); evidenceCounts.put(key, old == null ? 1 : old + 1); 1286 } 1287 for (FactGroup group : canonical) state.facts.add(materializeFact(group, evidenceCounts)); 1288 for (FactGroup group : candidates) state.factCandidates.add(materializeFact(group, evidenceCounts)); 1289 } 1290 1291 private SlpcObject materializeFact(FactGroup group, Map<String, Integer> counts) { 1292 int evidenceCount = counts.get(group.ownerKind + "\u0000" + group.ownerId); 1293 String resolution = group.canonical ? "RESOLVED" : "UNRESOLVED"; 1294 SlpcObject quality = qualitySummary(group.canonical ? "ELIGIBLE" : "INELIGIBLE", resolution, evidenceCount); 1295 SlpcObject.Builder result = SlpcObject.builder(); 1296 if (group.canonical) result.put("factId", group.semanticKey); 1297 else result.put("candidateId", group.ownerId).put("candidateFingerprint", group.semanticKey); 1298 result.put("sourceEndpointRef", group.semantic.get("sourceEndpointRef")) 1299 .put("targetEndpointRef", group.semantic.string("targetEndpointRef")) 1300 .put("axis", group.semantic.string("axis")).put("role", group.semantic.string("role")) 1301 .put("operationKind", group.semantic.string("operationKind")) 1302 .put("operationInputKind", group.semantic.string("operationInputKind")); 1303 if (!group.canonical) result.put("candidateReasons", Collections.singletonList(candidateReason(group.semantic))); 1304 return result.put("qualitySummary", quality).put("extensions", Collections.emptyList()).build(); 1305 } 1306 1307 private SlpcObject mergeOccurrence(SlpcObject left, SlpcObject right) { 1308 SlpcObject leftBase = left.toBuilder().remove("literalInputs").remove("supportPathRefs").remove("sourceLocations").build(); 1309 SlpcObject rightBase = right.toBuilder().remove("literalInputs").remove("supportPathRefs").remove("sourceLocations").build(); 1310 if (!leftBase.equals(rightBase)) throw new IllegalStateException("SLPC occurrence ID collision: " + left.string("occurrenceId")); 1311 Map<String, SlpcObject> literals = new TreeMap<String, SlpcObject>(); 1312 addLiterals(literals, left.array("literalInputs")); addLiterals(literals, right.array("literalInputs")); 1313 List<Object> merged = new ArrayList<Object>(); int ordinal = 1; 1314 for (SlpcObject literal : literals.values()) merged.add(literal.toBuilder().put("inputOrdinal", ordinal++).build()); 1315 SlpcObject.Builder result = left.toBuilder(); if (!merged.isEmpty()) result.put("literalInputs", merged); 1316 Map<String, SlpcObject> paths = new TreeMap<String, SlpcObject>(); 1317 addSupportPaths(paths, left.array("supportPathRefs")); addSupportPaths(paths, right.array("supportPathRefs")); 1318 if (!paths.isEmpty()) result.put("supportPathRefs", new ArrayList<Object>(paths.values())); 1319 Map<String, SlpcObject> locations = new TreeMap<String, SlpcObject>(); 1320 addSourceLocations(locations, left.array("sourceLocations")); addSourceLocations(locations, right.array("sourceLocations")); 1321 result.put("sourceLocations", new ArrayList<Object>(locations.values())); 1322 return result.build(); 1323 } 1324 1325 private void addSourceLocations(Map<String, SlpcObject> target, List<Object> values) { 1326 if (values == null) return; 1327 for (Object value : values) { 1328 SlpcObject location = (SlpcObject) value; target.put(sourceLocationKey(location), location); 1329 } 1330 } 1331 1332 private String sourceLocationKey(SlpcObject location) { 1333 return location.string("artifactId") + ":" + location.get("startLine") + ":" 1334 + location.get("startColumn") + ":" + location.get("endLine") + ":" + location.get("endColumn"); 1335 } 1336 1337 private void addSupportPaths(Map<String, SlpcObject> target, List<Object> values) { 1338 if (values == null) return; 1339 for (Object value : values) { 1340 SlpcObject support = (SlpcObject) value; 1341 target.put(support.string("graphRef") + "\u0000" + support.string("pathRef"), support); 1342 } 1343 } 1344 1345 private void addLiterals(Map<String, SlpcObject> target, List<Object> values) { 1346 if (values == null) return; 1347 for (Object value : values) { SlpcObject literal = (SlpcObject) value; SlpcObject fp = literal.object("fingerprint"); 1348 target.put(fp.string("algorithm") + "\u0000" + fp.string("value"), literal); } 1349 } 1350 1351 private SlpcObject occurrence(FactOccurrenceDraft draft, State state, String id) { 1352 SlpcObject owner = SlpcObject.builder().put("kind", draft.group.ownerKind).put("id", draft.group.ownerId).build(); 1353 SlpcObject origin = SlpcObject.builder().put("engineKind", "LEGACY_DLINEAGE").put("adapterName", ADAPTER_NAME).build(); 1354 SlpcObject quality = occurrenceQuality(draft.resolution); 1355 SlpcObject.Builder result = SlpcObject.builder().put("occurrenceId", id).put("ownerRef", owner) 1356 .put("artifactId", state.artifact.string("artifactId")).put("statementId", state.statementId) 1357 .put("evidenceKind", "STATIC_SQL").put("origin", origin) 1358 .putNull("procedureEndpointRef").putNull("procedureCanonicalEndpointId") 1359 .put("transformation", draft.transformation).put("predicate", draft.predicate) 1360 .put("quality", quality).put("sourceLocations", draft.sourceLocations) 1361 .put("extensions", Collections.emptyList()); 1362 if (draft.procedureEndpointRef != null) { 1363 result.put("procedureEndpointRef", draft.procedureEndpointRef) 1364 .put("procedureCanonicalEndpointId", 1365 state.endpointByRef.get(draft.procedureEndpointRef).string("canonicalEndpointId")); 1366 } 1367 if (!draft.literalInputs.isEmpty()) result.put("literalInputs", draft.literalInputs); 1368 if (!draft.supportPathRefs.isEmpty()) result.put("supportPathRefs", draft.supportPathRefs); 1369 return result.build(); 1370 } 1371 1372 private void materializeObjectRelationships(State state) { 1373 List<RelationshipGroup> canonical = new ArrayList<RelationshipGroup>(); 1374 List<RelationshipGroup> candidates = new ArrayList<RelationshipGroup>(); 1375 for (RelationshipGroup group : state.relationshipGroups.values()) { 1376 if (group.canonical) canonical.add(group); else candidates.add(group); 1377 } 1378 Collections.sort(canonical, relationshipGroupComparator()); 1379 Collections.sort(candidates, relationshipGroupComparator()); 1380 for (RelationshipGroup group : canonical) { 1381 group.ownerKind = "OBJECT_RELATIONSHIP"; 1382 group.ownerId = group.semanticKey; 1383 } 1384 for (int index = 0; index < candidates.size(); index++) { 1385 RelationshipGroup group = candidates.get(index); 1386 group.ownerKind = "OBJECT_RELATIONSHIP_CANDIDATE"; 1387 group.ownerId = local("objrel-cand-", index + 1); 1388 } 1389 Map<String, String> semanticKeys = new HashMap<String, String>(); 1390 for (RelationshipGroup group : canonical) semanticKeys.put(group.ownerKind + "\u0000" + group.ownerId, group.semanticKey); 1391 for (RelationshipGroup group : candidates) semanticKeys.put(group.ownerKind + "\u0000" + group.ownerId, group.semanticKey); 1392 Map<String, SlpcObject> newOccurrences = new TreeMap<String, SlpcObject>(); 1393 for (RelationshipGroup group : canonical) addRelationshipOccurrence(group, semanticKeys, state, newOccurrences); 1394 for (RelationshipGroup group : candidates) addRelationshipOccurrence(group, semanticKeys, state, newOccurrences); 1395 state.occurrences.addAll(newOccurrences.values()); 1396 Collections.sort(state.occurrences, new Comparator<Object>() { @Override public int compare(Object left, Object right) { 1397 return ((SlpcObject) left).string("occurrenceId").compareTo(((SlpcObject) right).string("occurrenceId")); 1398 }}); 1399 for (RelationshipGroup group : canonical) state.objectRelationships.add(materializeRelationship(group, false)); 1400 for (RelationshipGroup group : candidates) state.objectRelationshipCandidates.add(materializeRelationship(group, true)); 1401 } 1402 1403 private void addRelationshipOccurrence(RelationshipGroup group, Map<String, String> semanticKeys, 1404 State state, Map<String, SlpcObject> target) { 1405 SlpcObject owner = SlpcObject.builder().put("kind", group.ownerKind).put("id", group.ownerId).build(); 1406 SlpcObject origin = SlpcObject.builder().put("engineKind", "LEGACY_DLINEAGE") 1407 .put("adapterName", ADAPTER_NAME).build(); 1408 String resolution = group.canonical ? "RESOLVED" : "UNRESOLVED"; 1409 SlpcObject withoutId = SlpcObject.builder().put("occurrenceId", "slocc1-" + zeros(64)) 1410 .put("ownerRef", owner).put("artifactId", state.artifact.string("artifactId")) 1411 .put("statementId", state.statementId).put("evidenceKind", "STATIC_SQL") 1412 .put("origin", origin).putNull("procedureEndpointRef").putNull("procedureCanonicalEndpointId") 1413 .putNull("transformation").putNull("predicate").put("quality", occurrenceQuality(resolution)) 1414 .put("sourceLocations", new ArrayList<Object>(group.sourceLocations.values())) 1415 .put("extensions", Collections.emptyList()).build(); 1416 String id = SlpcIdentity.occurrenceId(withoutId, semanticKeys); 1417 target.put(id, withoutId.toBuilder().put("occurrenceId", id).build()); 1418 } 1419 1420 private SlpcObject materializeRelationship(RelationshipGroup group, boolean candidate) { 1421 SlpcObject.Builder result = SlpcObject.builder(); 1422 if (candidate) { 1423 result.put("candidateId", group.ownerId).put("candidateFingerprint", group.semanticKey); 1424 } else { 1425 result.put("relationshipId", group.semanticKey); 1426 } 1427 result.put("relationshipKind", group.semantic.string("relationshipKind")) 1428 .put("participants", group.semantic.array("participants")); 1429 if (group.semantic.array("keyFields") != null) result.put("keyFields", group.semantic.array("keyFields")); 1430 if (group.semantic.array("keyPairs") != null) result.put("keyPairs", group.semantic.array("keyPairs")); 1431 if (candidate) result.put("candidateReasons", Collections.singletonList("IDENTITY_UNAVAILABLE")); 1432 return result.put("qualitySummary", qualitySummary(candidate ? "INELIGIBLE" : "ELIGIBLE", 1433 candidate ? "UNRESOLVED" : "RESOLVED", 1)) 1434 .put("extensions", Collections.emptyList()).build(); 1435 } 1436 1437 private void buildIntermediateGraph(dataflow legacy, State state) { 1438 List<table> intermediate = new ArrayList<table>(); 1439 for (table relation : dataflow.getAllTables(legacy)) if (isIntermediate(relation)) intermediate.add(relation); 1440 if (intermediate.isEmpty()) { 1441 for (relationship relation : legacy.getRelationships()) if ("fddi".equals(relation.getType())) { 1442 accountDedicatedLoss(relation, state, "SLPC_INTERMEDIATE_HOP_UNCOLLAPSIBLE", 1443 "Legacy fddi has no retained intermediate topology against which to verify a boundary path."); 1444 } 1445 return; 1446 } 1447 Collections.sort(intermediate, tableComparator()); 1448 1449 Map<String, table> tableById = new HashMap<String, table>(); 1450 for (table relation : intermediate) if (relation.getId() != null) tableById.put(relation.getId(), relation); 1451 IdentityHashMap<table, table> parentByTable = new IdentityHashMap<table, table>(); 1452 for (table relation : intermediate) { 1453 if (relation.getParent() == null) continue; 1454 table parent = tableById.get(relation.getParent()); 1455 if (parent == null || parent == relation) { 1456 containmentLoss(relation, state, 1457 "Legacy parent does not identify a distinct intermediate object; the node is retained as a root."); 1458 } else { 1459 parentByTable.put(relation, parent); 1460 } 1461 } 1462 breakContainmentCycles(intermediate, parentByTable, state); 1463 1464 IdentityHashMap<table, List<table>> childrenByTable = new IdentityHashMap<table, List<table>>(); 1465 for (table relation : intermediate) childrenByTable.put(relation, new ArrayList<table>()); 1466 List<table> roots = new ArrayList<table>(); 1467 for (table relation : intermediate) { 1468 table parent = parentByTable.get(relation); 1469 if (parent == null) roots.add(relation); else childrenByTable.get(parent).add(relation); 1470 } 1471 Collections.sort(roots, sourceTableComparator()); 1472 for (List<table> children : childrenByTable.values()) Collections.sort(children, sourceTableComparator()); 1473 1474 List<NodeDraft> nodeDrafts = new ArrayList<NodeDraft>(); 1475 Map<table, NodeDraft> tableNodes = new IdentityHashMap<table, NodeDraft>(); 1476 Map<column, NodeDraft> fieldNodes = new IdentityHashMap<column, NodeDraft>(); 1477 int ordinal = 1; 1478 ArrayDeque<PendingNode> stack = new ArrayDeque<PendingNode>(); 1479 for (int index = roots.size() - 1; index >= 0; index--) stack.push(new PendingNode(roots.get(index), null, null)); 1480 while (!stack.isEmpty()) { 1481 PendingNode pending = stack.pop(); 1482 if (pending.field != null) { 1483 NodeDraft fieldNode = new NodeDraft(pending.relation, pending.field, ordinal++, 1484 nodeKindForField(pending.relation), null, pending.parent); 1485 nodeDrafts.add(fieldNode); fieldNodes.put(pending.field, fieldNode); 1486 putNodeLegacyId(state.intermediateNodeByLegacyId, pending.field.getId(), fieldNode); 1487 continue; 1488 } 1489 NodeDraft relationNode = new NodeDraft(pending.relation, null, ordinal++, nodeKind(pending.relation), 1490 provenLocalName(pending.relation), pending.parent); 1491 nodeDrafts.add(relationNode); tableNodes.put(pending.relation, relationNode); 1492 putNodeLegacyId(state.intermediateNodeByLegacyId, pending.relation.getId(), relationNode); 1493 1494 List<PendingNode> children = new ArrayList<PendingNode>(); 1495 for (column field : pending.relation.getColumns()) { 1496 if (isRelationRows(field)) { 1497 putNodeLegacyId(state.intermediateNodeByLegacyId, field.getId(), relationNode); 1498 } else { 1499 children.add(new PendingNode(pending.relation, field, relationNode)); 1500 } 1501 } 1502 for (table child : childrenByTable.get(pending.relation)) children.add(new PendingNode(child, null, relationNode)); 1503 Collections.sort(children, pendingNodeComparator()); 1504 for (int index = children.size() - 1; index >= 0; index--) stack.push(children.get(index)); 1505 } 1506 state.intermediateTableNodes.putAll(tableNodes); state.intermediateColumnNodes.putAll(fieldNodes); 1507 1508 SlpcObject provisionalGraph = graphShell(state, "sliggfp1-" + zeros(64), Collections.<Object>emptyList(), 1509 Collections.<Object>emptyList()); 1510 List<Object> provisionalNodes = new ArrayList<Object>(); 1511 for (NodeDraft draft : nodeDrafts) { 1512 draft.provisionalRef = "tmp-node-" + draft.ordinal; 1513 SlpcObject node = draft.node(draft.provisionalRef, draft.parent == null ? null : draft.parent.provisionalRef, "slignfp2-" + zeros(64)); 1514 provisionalNodes.add(node); draft.provisionalNode = node; 1515 } 1516 provisionalGraph = graphShell(state, "sliggfp1-" + zeros(64), provisionalNodes, Collections.<Object>emptyList()); 1517 for (NodeDraft draft : nodeDrafts) { 1518 draft.fingerprint = SlpcIdentity.intermediateNodeFingerprint(draft.provisionalNode, provisionalGraph, state.endpointByRef); 1519 } 1520 Collections.sort(nodeDrafts, nodeFingerprintComparator()); 1521 for (int index = 0; index < nodeDrafts.size(); index++) { 1522 NodeDraft draft = nodeDrafts.get(index); draft.finalRef = local("ign-", index + 1); 1523 if (draft.field == null) state.intermediateTableRefs.put(draft.relation, draft.finalRef); 1524 else state.intermediateColumnRefs.put(draft.field, draft.finalRef); 1525 } 1526 List<Object> nodes = new ArrayList<Object>(); 1527 for (NodeDraft draft : nodeDrafts) nodes.add(draft.node(draft.finalRef, draft.parent == null ? null : draft.parent.finalRef, draft.fingerprint)); 1528 1529 List<EdgeDraft> edges = new ArrayList<EdgeDraft>(); 1530 for (relationship relation : legacy.getRelationships()) { 1531 if (!"fdd".equals(relation.getType()) && !"fdr".equals(relation.getType()) 1532 && !"frd".equals(relation.getType())) continue; 1533 if (state.dedicatedRelationshipAccounting.contains(legacyId(relation))) continue; 1534 if (unsupportedFactEffect(relation.getEffectType())) continue; 1535 for (sourceColumn source : relation.getSources()) { 1536 if (hasCandidateParents(source)) continue; 1537 FactSemantics semantics = semantics(relation, source); 1538 if (!semantics.publish) continue; 1539 SlpcObject target = targetAnchor(relation.getTarget(), semantics, state); 1540 if (target == null) continue; 1541 SlpcObject sourceAnchor = sourceAnchor(source, semantics, state); 1542 if (sourceAnchor == null) continue; 1543 // Pure physical-to-physical edges belong to facts, not to the intermediate graph. 1544 if ("ENDPOINT".equals(sourceAnchor.string("kind")) && "ENDPOINT".equals(target.string("kind"))) continue; 1545 edges.add(new EdgeDraft(relation, source, sourceAnchor, target, semantics, 1546 transformationEvidence(relation, source), evidence(predicateText(relation, semantics)))); 1547 } 1548 } 1549 Map<String, SlpcObject> nodeIndex = SlpcIdentity.index(nodes, "nodeRef"); 1550 for (EdgeDraft edge : edges) { 1551 SlpcObject provisional = edge.edge("ige-000001", "sligefp1-" + zeros(64)); 1552 edge.fingerprint = SlpcIdentity.intermediateEdgeFingerprint(provisional, state.endpointByRef, nodeIndex); 1553 } 1554 Map<String, EdgeDraft> uniqueEdges = new TreeMap<String, EdgeDraft>(); 1555 for (EdgeDraft edge : edges) { 1556 EdgeDraft previous = uniqueEdges.put(edge.fingerprint, edge); 1557 if (previous != null && !previous.sameSemantic(edge)) { 1558 throw new IllegalStateException("SLPC intermediate edge fingerprint collision: " + edge.fingerprint); 1559 } 1560 } 1561 edges = new ArrayList<EdgeDraft>(uniqueEdges.values()); 1562 Collections.sort(edges, edgeFingerprintComparator()); 1563 List<Object> edgeRecords = new ArrayList<Object>(); 1564 for (int index = 0; index < edges.size(); index++) { 1565 EdgeDraft edge = edges.get(index); edge.finalRef = local("ige-", index + 1); 1566 edgeRecords.add(edge.edge(edge.finalRef, edge.fingerprint)); 1567 } 1568 Map<String, SlpcObject> edgeIndex = SlpcIdentity.index(edgeRecords, "edgeRef"); 1569 List<PathDraft> paths = enumeratePaths(edges, state); 1570 for (PathDraft path : paths) { 1571 SlpcObject provisional = path.path("igp-000001", "sligpfp1-" + zeros(64)); 1572 path.fingerprint = SlpcIdentity.intermediatePathFingerprint(provisional, edgeIndex); 1573 } 1574 Collections.sort(paths, pathFingerprintComparator()); 1575 List<Object> pathRecords = new ArrayList<Object>(); 1576 for (int index = 0; index < paths.size(); index++) { 1577 PathDraft path = paths.get(index); path.finalRef = local("igp-", index + 1); 1578 pathRecords.add(path.path(path.finalRef, path.fingerprint)); 1579 } 1580 SlpcObject graph = graphShell(state, "sliggfp1-" + zeros(64), nodes, edgeRecords) 1581 .toBuilder().put("paths", pathRecords).build(); 1582 graph = graph.toBuilder().put("graphFingerprint", SlpcIdentity.intermediateGraphFingerprint(graph)).build(); 1583 List<Object> graphExtensions = interactiveExplainExtensions(legacy, edges, graph, state); 1584 if (!graphExtensions.isEmpty()) graph = graph.toBuilder().put("extensions", graphExtensions).build(); 1585 state.graphs.add(graph); 1586 collapsePhysicalPaths(paths, state); 1587 accountFddiAgainstPaths(legacy, paths, state); 1588 state.diagnostics.add(diagnostic("SLPC_INTERMEDIATE_GRAPH_INCOMPLETE", "WARN", "INTERMEDIATE_GRAPH", "graph-000001", 1589 "Legacy result-set containment, proven edges, and bounded acyclic boundary paths are preserved; the legacy POJO still does not prove globally exhaustive analysis.")); 1590 if (graphExtensions.isEmpty()) { 1591 state.diagnostics.add(diagnostic("SLPC_ADAPTER_CAPABILITY_GAP", "INFO", "INTERMEDIATE_GRAPH", "graph-000001", 1592 "No authoritative typed-operation/ordered-operand evidence was supplied, so no interactive explain profile is emitted.")); 1593 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 1594 "graph-000001", "GSP_LEGACY_INTERACTIVE_EXPLAIN_UNAVAILABLE", 1595 "Clickable topology is retained, but typed steps require producer evidence supplied through LegacyAdapterContext.")); 1596 } 1597 } 1598 1599 private List<PathDraft> enumeratePaths(List<EdgeDraft> edges, State state) { 1600 Map<String, List<EdgeDraft>> outgoing = new HashMap<String, List<EdgeDraft>>(); 1601 List<EdgeDraft> starts = new ArrayList<EdgeDraft>(); 1602 for (EdgeDraft edge : edges) { 1603 String key = anchorKey(edge.source); 1604 if (!outgoing.containsKey(key)) outgoing.put(key, new ArrayList<EdgeDraft>()); 1605 outgoing.get(key).add(edge); 1606 if ("ENDPOINT".equals(edge.source.string("kind"))) starts.add(edge); 1607 } 1608 for (List<EdgeDraft> values : outgoing.values()) Collections.sort(values, edgeFingerprintComparator()); 1609 Collections.sort(starts, edgeFingerprintComparator()); 1610 Map<String, PathDraft> unique = new TreeMap<String, PathDraft>(); 1611 ArrayDeque<PathWalk> stack = new ArrayDeque<PathWalk>(); 1612 for (int index = starts.size() - 1; index >= 0; index--) stack.push(PathWalk.start(starts.get(index))); 1613 int expansions = 0; 1614 while (!stack.isEmpty()) { 1615 if (++expansions > 100000) { state.pathEnumerationTruncated = true; break; } 1616 PathWalk walk = stack.pop(); EdgeDraft last = walk.edges.get(walk.edges.size() - 1); 1617 if ("ENDPOINT".equals(last.target.string("kind"))) { 1618 String key = walk.edgeKey(); if (!unique.containsKey(key)) unique.put(key, new PathDraft(walk.edges)); 1619 if (unique.size() >= 10000) { state.pathEnumerationTruncated = true; break; } 1620 continue; 1621 } 1622 List<EdgeDraft> next = outgoing.get(anchorKey(last.target)); 1623 if (next == null) continue; 1624 for (int index = next.size() - 1; index >= 0; index--) { 1625 EdgeDraft edge = next.get(index); 1626 if (walk.used.contains(edge.finalRef)) { state.pathCycleObserved = true; continue; } 1627 stack.push(walk.append(edge)); 1628 } 1629 } 1630 if (state.pathCycleObserved || state.pathEnumerationTruncated) { 1631 state.diagnostics.add(diagnostic("SLPC_INTERMEDIATE_HOP_UNCOLLAPSIBLE", "WARN", "DOCUMENT", null, 1632 state.pathEnumerationTruncated 1633 ? "Intermediate path enumeration reached its deterministic path/expansion safety cap." 1634 : "A cycle was detected during path enumeration; cyclic continuations were not collapsed.")); 1635 } 1636 return new ArrayList<PathDraft>(unique.values()); 1637 } 1638 1639 private String anchorKey(SlpcObject anchor) { 1640 String ref = "ENDPOINT".equals(anchor.string("kind")) ? anchor.string("endpointRef") : anchor.string("nodeRef"); 1641 return anchor.string("kind") + "\u0000" + ref + "\u0000" + anchor.string("portCode"); 1642 } 1643 1644 private void collapsePhysicalPaths(List<PathDraft> paths, State state) { 1645 int collapsed = 0; 1646 for (PathDraft path : paths) { 1647 FactSemantics semantics = collapsedSemantics(path); 1648 if (semantics == null) continue; 1649 String sourceRef = path.sourceEndpointRef(), targetRef = path.targetEndpointRef(); 1650 if (sourceRef == null || targetRef == null || sourceRef.equals(targetRef)) continue; 1651 SlpcObject semantic = semanticRecord(sourceRef, targetRef, semantics, state); 1652 FactGroup group = factGroup(semantic, state); 1653 List<Object> support = Collections.<Object>singletonList(SlpcObject.builder() 1654 .put("graphRef", "graph-000001").put("pathRef", path.finalRef).build()); 1655 FactOccurrenceDraft occurrence = new FactOccurrenceDraft(group, null, null, 1656 group.canonical ? "RESOLVED" : "UNRESOLVED", Collections.<Object>emptyList(), support, 1657 pathSourceLocations(path, state), null); 1658 group.occurrences.add(occurrence); state.factOccurrenceDrafts.add(occurrence); collapsed++; 1659 } 1660 if (collapsed > 0) { 1661 state.hasPhysicalHopCollapse = true; 1662 state.diagnostics.add(diagnostic("SLPC_INTERMEDIATE_HOP_COLLAPSED", "INFO", "DOCUMENT", null, 1663 "Published " + collapsed + " physical-boundary lineage occurrence(s) with auditable supportPathRefs.")); 1664 } 1665 } 1666 1667 private List<Object> pathSourceLocations(PathDraft path, State state) { 1668 Map<String, SlpcObject> result = new TreeMap<String, SlpcObject>(); 1669 for (EdgeDraft edge : path.edges) addSourceLocations(result, sourceLocations(edge.relation, state)); 1670 return new ArrayList<Object>(result.values()); 1671 } 1672 1673 private FactGroup factGroup(SlpcObject semantic, State state) { 1674 boolean canonical = isCanonicalFact(semantic, state.endpointByRef); 1675 String key = canonical ? SlpcIdentity.factId(semantic, state.endpointByRef) 1676 : SlpcIdentity.factCandidateFingerprint(semantic, state.endpointByRef); 1677 FactGroup group = state.factGroups.get(key); 1678 if (group == null) { group = new FactGroup(semantic, canonical, key); state.factGroups.put(key, group); } 1679 else if (!group.sameSemantic(semantic)) throw new IllegalStateException("SLPC fact fingerprint collision: " + key); 1680 return group; 1681 } 1682 1683 private FactSemantics collapsedSemantics(PathDraft path) { 1684 boolean changed = false, aggregate = false; 1685 for (EdgeDraft edge : path.edges) { 1686 if (!"VALUE".equals(edge.semantics.axis) || !"NONE".equals(edge.semantics.operationKind)) return null; 1687 if (!equalsAny(edge.semantics.role, "DIRECT", "TRANSFORM", "AGGREGATE")) return null; 1688 changed |= !"DIRECT".equals(edge.semantics.role); aggregate |= "AGGREGATE".equals(edge.semantics.role); 1689 } 1690 String role = !changed ? "DIRECT" : aggregate && "AGGREGATE".equals(path.edges.get(path.edges.size() - 1).semantics.role) 1691 ? "AGGREGATE" : "TRANSFORM"; 1692 return FactSemantics.publish("VALUE", role, "NONE", "NONE", false); 1693 } 1694 1695 private void accountFddiAgainstPaths(dataflow legacy, List<PathDraft> paths, State state) { 1696 for (relationship relation : legacy.getRelationships()) { 1697 if (!"fddi".equals(relation.getType())) continue; 1698 boolean matched = false; 1699 for (sourceColumn source : relation.getSources()) { 1700 for (String axis : new String[] {"VALUE", "ROW"}) { 1701 FactSemantics semantics = FactSemantics.publish(axis, 1702 "VALUE".equals(axis) ? "TRANSFORM" : "UNKNOWN", "NONE", "NONE", false); 1703 String sourceRef = resolveSource(source, semantics, state); 1704 String targetRef = resolveTarget(relation.getTarget(), semantics, state); 1705 if (sourceRef == null || targetRef == null) continue; 1706 for (PathDraft path : paths) { 1707 if (sourceRef.equals(path.sourceEndpointRef()) && targetRef.equals(path.targetEndpointRef()) 1708 && axis.equals(path.axis())) { matched = true; break; } 1709 } 1710 if (matched) break; 1711 } 1712 if (matched) break; 1713 } 1714 if (matched) markDedicatedMapped(relation, state, "supportPathRefs"); 1715 else accountDedicatedLoss(relation, state, "SLPC_INTERMEDIATE_HOP_UNCOLLAPSIBLE", 1716 "Legacy fddi was not published by direction-by-name; no matching proven boundary path was found."); 1717 } 1718 } 1719 1720 private List<Object> interactiveExplainExtensions(dataflow legacy, List<EdgeDraft> edges, 1721 SlpcObject graph, State state) { 1722 List<StepDraft> drafts = new ArrayList<StepDraft>(); 1723 Set<String> globallyUsedEdges = new HashSet<String>(); 1724 for (relationship relation : legacy.getRelationships()) { 1725 LegacyAdapterContext.TypedTransform typed = state.context.typedTransformResolver().resolve(relation); 1726 if (typed == null) continue; 1727 if (!equalsAny(typed.operationFamily(), "ARITHMETIC", "COMPARISON", "BOOLEAN", "FUNCTION", "CAST", 1728 "CASE", "CONCAT", "AGGREGATE", "WINDOW", "CONSTRUCTOR", "CONTROL_FLOW", "OTHER") 1729 || !typed.operationCode().matches("^[A-Z][A-Z0-9_]*$")) { 1730 accountSemanticGap(relation, "SLPC_EXTENSION_UNSUPPORTED", 1731 "Typed transform resolver returned an operation family/code outside the v0.2 registry.", state); 1732 continue; 1733 } 1734 List<EdgeDraft> relationEdges = new ArrayList<EdgeDraft>(); 1735 for (EdgeDraft edge : edges) if (edge.relation == relation) relationEdges.add(edge); 1736 List<Object> bindings = new ArrayList<Object>(); SlpcObject resultAnchor = null; boolean valid = true; 1737 Set<String> localEdges = new HashSet<String>(); int ordinal = 1; 1738 for (LegacyAdapterContext.TypedOperand operand : typed.operands()) { 1739 if (!operand.roleCode().matches("^[A-Z][A-Z0-9_]*$")) { valid = false; break; } 1740 EdgeDraft match = null; 1741 for (EdgeDraft edge : relationEdges) { 1742 if (sourceMatches(edge.sourceColumn, operand.sourceId())) { 1743 if (match != null) { valid = false; break; } 1744 match = edge; 1745 } 1746 } 1747 if (!valid || match == null || !localEdges.add(match.finalRef) 1748 || globallyUsedEdges.contains(match.finalRef) 1749 || (resultAnchor != null && !resultAnchor.equals(match.target))) { valid = false; break; } 1750 resultAnchor = match.target; 1751 bindings.add(SlpcObject.builder().put("ordinal", ordinal++).put("roleCode", operand.roleCode()) 1752 .put("edgeRef", match.finalRef).build()); 1753 } 1754 if (!valid || bindings.isEmpty()) { 1755 accountSemanticGap(relation, "SLPC_EXTENSION_UNSUPPORTED", 1756 "Typed operands did not bind one-to-one to this relationship's graph edges and common result anchor.", state); 1757 continue; 1758 } 1759 globallyUsedEdges.addAll(localEdges); 1760 drafts.add(new StepDraft(typed.operationFamily(), typed.operationCode(), bindings, resultAnchor, 1761 relationEdges.isEmpty() ? null : relationEdges.get(0).transformation)); 1762 } 1763 if (drafts.isEmpty()) return Collections.emptyList(); 1764 Collections.sort(drafts, new Comparator<StepDraft>() { @Override public int compare(StepDraft left, StepDraft right) { 1765 return left.sortKey().compareTo(right.sortKey()); 1766 }}); 1767 Map<String, SlpcObject> nodeIndex = SlpcIdentity.index(graph.array("nodes"), "nodeRef"); 1768 Map<String, SlpcObject> edgeIndex = SlpcIdentity.index(graph.array("edges"), "edgeRef"); 1769 List<Object> steps = new ArrayList<Object>(); 1770 for (int index = 0; index < drafts.size(); index++) { 1771 StepDraft draft = drafts.get(index); int ordinal = index + 1; 1772 SlpcObject provisional = draft.step(local("igs-", ordinal), "sligsfp1-" + zeros(64), ordinal); 1773 String fingerprint = SlpcIdentity.interactiveStepFingerprint(provisional, graph, 1774 state.endpointByRef, nodeIndex, edgeIndex); 1775 steps.add(draft.step(local("igs-", ordinal), fingerprint, ordinal)); 1776 } 1777 state.hasInteractiveExplain = true; 1778 return Collections.<Object>singletonList(SlpcObject.builder() 1779 .put("extensionType", "gsp.interactive-explain.v1").put("schemaVersion", "1.0.0") 1780 .put("criticality", "OPTIONAL").put("payload", SlpcObject.builder().put("steps", steps).build()).build()); 1781 } 1782 1783 private boolean sourceMatches(sourceColumn source, String sourceId) { 1784 return sourceId.equals(source.getId()) || sourceId.equals(source.getSource_id()); 1785 } 1786 1787 private void buildJoinSemantics(dataflow legacy, State state) { 1788 List<SlpcObject> records = new ArrayList<SlpcObject>(); int ordinal = 1; 1789 for (relationship relation : legacy.getRelationships()) { 1790 if (!"join".equals(relation.getType())) continue; 1791 if (relation.getTarget() == null || relation.getSources().size() != 1) { 1792 accountLoss(relation, state, "SLPC_JOIN_SEMANTICS_PARTIAL", 1793 "Dedicated legacy join record does not contain exactly one typed source/target pair."); 1794 continue; 1795 } 1796 sourceColumn left = relation.getSources().get(0); 1797 String leftField = resolveSourceField(left, state), rightField = resolveTargetField(relation.getTarget(), state); 1798 String leftRelation = relationEndpoint(left.getParent_id(), state); 1799 String rightRelation = relationEndpoint(relation.getTarget().getParent_id(), state); 1800 if (leftField == null || rightField == null || leftRelation == null || rightRelation == null) { 1801 accountLoss(relation, state, "SLPC_ENDPOINT_UNRESOLVED", 1802 "Dedicated join participants could not all be bound without endpoint-level coarsening."); 1803 continue; 1804 } 1805 String joinType = joinType(relation.getJoinType()); 1806 if ("UNKNOWN".equals(joinType)) { 1807 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 1808 legacyId(relation), "SLPC_JOIN_SEMANTICS_PARTIAL", 1809 "The dedicated join record proves its participants but its join-type wire code is outside the SLPC registry.")); 1810 } 1811 List<Object> keyPairs = "CROSS".equals(joinType) ? Collections.<Object>emptyList() 1812 : Collections.<Object>singletonList(SlpcObject.builder().put("ordinal", 1) 1813 .put("leftFieldEndpointRef", leftField).put("rightFieldEndpointRef", rightField).put("operator", "OTHER").build()); 1814 accountSemanticGap(relation, "SLPC_JOIN_SEMANTICS_PARTIAL", 1815 "Dedicated join records do not carry a stable occurrence link to fdr records or a typed comparison operator; refinesFact arrays remain empty.", state); 1816 String conditionForm = conditionForm(relation.getClause(), relation.getJoinType(), joinType); 1817 SlpcObject provisional = SlpcObject.builder().put("joinSemanticsId", "sljoin1-" + zeros(64)) 1818 .put("statementId", state.statementId).put("joinOrdinal", ordinal++).put("joinType", joinType) 1819 .put("conditionForm", conditionForm).put("leftRelationEndpointRef", leftRelation) 1820 .put("rightRelationEndpointRef", rightRelation).put("leftSideSemantics", leftSide(joinType)) 1821 .put("rightSideSemantics", rightSide(joinType)).put("keyPairs", keyPairs) 1822 .put("predicate", "CROSS".equals(joinType) ? null : evidence(nonEmpty(relation.getCondition()))) 1823 .put("refinesFactIds", Collections.emptyList()) 1824 .put("refinesFactCandidateFingerprints", Collections.emptyList()) 1825 .put("quality", quality(participantsCanonicalRefs(Arrays2(leftRelation, rightRelation, leftField, rightField), state) ? "PARTIAL" : "UNRESOLVED", 1826 "HEURISTIC", "INFERRED", 900000)) 1827 .put("sourceLocations", sourceLocations(relation, state)) 1828 .put("extensions", Collections.emptyList()).build(); 1829 SlpcObject record = provisional.toBuilder().put("joinSemanticsId", SlpcIdentity.joinSemanticsId(provisional, state.endpointByRef)).build(); 1830 records.add(record); 1831 state.coverage.add(new LegacyCoverageReport.Entry("RELATIONSHIP", legacyId(relation), 1832 LegacyCoverageReport.Outcome.CONDITIONAL, 1833 record.string("joinSemanticsId"), "dedicated join record; operator/refinement correlation remains partial")); 1834 } 1835 Collections.sort(records, new Comparator<SlpcObject>() { @Override public int compare(SlpcObject left, SlpcObject right) { 1836 return left.string("joinSemanticsId").compareTo(right.string("joinSemanticsId")); 1837 }}); 1838 state.joinSemantics.addAll(records); 1839 } 1840 1841 private String joinType(String value) { 1842 if (value == null) return "UNKNOWN"; 1843 String normalized = value.toLowerCase(java.util.Locale.ROOT); 1844 if (equalsAny(normalized, "left", "leftouter", "natural_left", "natural_leftouter")) return "LEFT"; 1845 if (equalsAny(normalized, "right", "rightouter", "natural_right", "natural_rightouter")) return "RIGHT"; 1846 if (equalsAny(normalized, "full", "fullouter", "natural_full", "natural_fullouter")) return "FULL"; 1847 if ("cross".equals(normalized)) return "CROSS"; 1848 if (equalsAny(normalized, "semi", "leftsemi", "rightsemi")) return "SEMI"; 1849 if (equalsAny(normalized, "anti", "leftanti", "rightanti")) return "ANTI"; 1850 if ("asof".equals(normalized)) return "ASOF"; 1851 if (equalsAny(normalized, "inner", "join", "natural", "natural_inner", "straight")) return "INNER"; 1852 return "UNKNOWN"; 1853 } 1854 1855 private String conditionForm(String clause, String legacyJoinType, String joinType) { 1856 if ("CROSS".equals(joinType)) return "NONE"; 1857 String rawType = safe(legacyJoinType).toLowerCase(java.util.Locale.ROOT); 1858 if (rawType.equals("natural") || rawType.startsWith("natural_")) return "NATURAL"; 1859 if ("on".equals(clause)) return "ON"; 1860 return nonEmpty(clause) == null ? "NONE" : "OTHER"; 1861 } 1862 1863 private String leftSide(String joinType) { 1864 if ("LEFT".equals(joinType) || "SEMI".equals(joinType) || "ANTI".equals(joinType)) return "PRESERVED"; 1865 if ("RIGHT".equals(joinType) || "FULL".equals(joinType)) return "NULL_EXTENDED"; 1866 if ("INNER".equals(joinType)) return "FILTERED"; 1867 if ("CROSS".equals(joinType)) return "NOT_APPLICABLE"; 1868 return "UNKNOWN"; 1869 } 1870 1871 private String rightSide(String joinType) { 1872 if ("RIGHT".equals(joinType)) return "PRESERVED"; if ("LEFT".equals(joinType) || "FULL".equals(joinType)) return "NULL_EXTENDED"; 1873 if ("INNER".equals(joinType) || "SEMI".equals(joinType) || "ANTI".equals(joinType)) return "FILTERED"; 1874 if ("CROSS".equals(joinType)) return "NOT_APPLICABLE"; return "UNKNOWN"; 1875 } 1876 1877 private boolean participantsCanonicalRefs(List<String> refs, State state) { 1878 for (String ref : refs) if (state.endpointByRef.get(ref).string("canonicalEndpointId") == null) return false; 1879 return true; 1880 } 1881 1882 private List<String> Arrays2(String... values) { 1883 List<String> result = new ArrayList<String>(); Collections.addAll(result, values); return result; 1884 } 1885 1886 private SlpcObject graphShell(State state, String fingerprint, List<Object> nodes, List<Object> edges) { 1887 return SlpcObject.builder().put("graphRef", "graph-000001").put("graphFingerprint", fingerprint) 1888 .put("statementId", state.statementId).put("graphOrdinal", 1).put("graphKind", "QUERY_BLOCK") 1889 .put("analysisCompleteness", "PARTIAL").put("nodes", nodes).put("edges", edges) 1890 .put("paths", Collections.emptyList()).put("extensions", Collections.emptyList()).build(); 1891 } 1892 1893 private SlpcObject semanticRecord(String sourceRef, String targetRef, FactSemantics semantics, State state) { 1894 return SlpcObject.builder().put("sourceEndpointRef", sourceRef).put("targetEndpointRef", targetRef) 1895 .put("axis", semantics.axis).put("role", semantics.role).put("operationKind", semantics.operationKind) 1896 .put("operationInputKind", semantics.operationInputKind).build(); 1897 } 1898 1899 private FactSemantics semantics(relationship relation, sourceColumn source) { 1900 if ("fdd".equals(relation.getType())) { 1901 if ("constant".equals(source.getColumn_type())) return FactSemantics.publish("VALUE", "DERIVED", "NONE", "NONE", true); 1902 boolean transformed = (source.getTransforms() != null && !source.getTransforms().isEmpty()) || nonEmpty(relation.getFunction()) != null; 1903 return FactSemantics.publish("VALUE", transformed ? "TRANSFORM" : "DIRECT", "NONE", "NONE", false); 1904 } 1905 if ("frd".equals(relation.getType())) { 1906 targetColumn target = relation.getTarget(); 1907 if (target != null && ("system".equals(target.getSource()) || "RelationRows".equals(target.getName()))) { 1908 return FactSemantics.publishWithGap("ROW", "UNKNOWN", "NONE", "NONE", false, 1909 "SLPC_ROLE_UNKNOWN", "Legacy frd RelationRows target proves row influence but not a narrower row role."); 1910 } 1911 if (nonEmpty(relation.getFunction()) != null || "function".equals(relation.getEffectType())) { 1912 return FactSemantics.publish("VALUE", "AGGREGATE", "NONE", "NONE", false); 1913 } 1914 return FactSemantics.reject("SLPC_RELATIONSHIP_UNMAPPED", 1915 "Legacy frd shape is neither an aggregate value result nor a RelationRows target; direction is quarantined."); 1916 } 1917 String clause = source.getClauseType(); 1918 if (equalsAny(clause, "joinCondition", "join")) return FactSemantics.publish("ROW", "JOIN", "NONE", "NONE", false); 1919 if ("groupby".equals(clause)) return FactSemantics.publish("ROW", "UNKNOWN", "GROUP_BY", "GROUP_KEY", false); 1920 if (equalsAny(clause, "where", "having", "qualify", "top", "limit", "sample", "filter")) 1921 return FactSemantics.publish("ROW", "FILTER", "NONE", "NONE", false); 1922 if (equalsAny(clause, "orderby", "sortby")) { 1923 return FactSemantics.reject("SLPC_OPERATION_UNSUPPORTED", 1924 "Legacy ORDER BY/SORT BY evidence requires DatasetOperationInfluence(SORT); it must not be published as a ROW fact."); 1925 } 1926 return FactSemantics.publishWithGap("ROW", "UNKNOWN", "NONE", "NONE", false, 1927 "SLPC_OPERATION_UNSUPPORTED", "Unrecognized or absent ESqlClause is retained only as ROW/UNKNOWN."); 1928 } 1929 1930 private SlpcObject transformationEvidence(relationship relation, sourceColumn source) { 1931 if (source.getTransforms() != null && !source.getTransforms().isEmpty()) { 1932 List<String> values = new ArrayList<String>(); 1933 for (transform value : source.getTransforms()) if (value.getCode() != null) values.add(value.getCode()); 1934 if (!values.isEmpty()) { 1935 List<byte[]> frames = new ArrayList<byte[]>(); 1936 for (String value : values) frames.add(SlpcIdentity.string(value)); 1937 return evidence("GSP-LEGACY-TRANSFORM-SEQUENCE-SHA256-V1", 1938 SlpcIdentity.framedList(frames)); 1939 } 1940 } 1941 return evidence(nonEmpty(relation.getFunction())); 1942 } 1943 1944 private String predicateText(relationship relation, FactSemantics semantics) { 1945 return "ROW".equals(semantics.axis) ? nonEmpty(relation.getCondition()) : null; 1946 } 1947 1948 private SlpcObject evidence(String text) { 1949 if (text == null) return null; 1950 // Persist only the hash: legacy text may include literals or credentials and has no redaction provenance. 1951 return evidence("RAW-UTF8-SHA256-V1", text.getBytes(StandardCharsets.UTF_8)); 1952 } 1953 1954 private SlpcObject evidence(String algorithm, byte[] bytes) { 1955 SlpcObject fingerprint = SlpcObject.builder().put("algorithm", algorithm) 1956 .put("value", sha256(bytes)).build(); 1957 return SlpcObject.builder().put("fingerprint", fingerprint).putNull("sourceLocation") 1958 .put("locationNullReason", "UNKNOWN").build(); 1959 } 1960 1961 private SlpcObject artifact(LegacyAdapterContext context) { 1962 SlpcObject hash = SlpcObject.builder().put("algorithm", "SHA256").put("value", context.contentSha256()).build(); 1963 SlpcObject withoutId = SlpcObject.builder().put("artifactId", "slart1-" + zeros(64)) 1964 .put("sourceKind", context.sourceKind()).put("logicalLocator", context.logicalLocator()) 1965 .put("sqlDialect", context.sqlDialect()).put("contentHash", hash).put("mediaType", "application/sql").build(); 1966 return withoutId.toBuilder().put("artifactId", SlpcIdentity.artifactId(withoutId)).build(); 1967 } 1968 1969 private SlpcObject statement(SlpcObject artifact, LegacyAdapterContext context, dataflow legacy) { 1970 SlpcObject.Builder builder = SlpcObject.builder().put("statementId", "slstmt1-" + zeros(64)) 1971 .put("artifactId", artifact.string("artifactId")).put("statementIndex", 1) 1972 .put("kind", context.statementKind()); 1973 SlpcObject location = statementSourceLocation(artifact.string("artifactId"), legacy); 1974 if (location != null) builder.put("sourceLocation", location); 1975 SlpcObject withoutId = builder.build(); 1976 return withoutId.toBuilder().put("statementId", SlpcIdentity.statementId(withoutId)).build(); 1977 } 1978 1979 private List<Object> statementSourceLocations(SlpcObject statement) { 1980 Object value = statement.get("sourceLocation"); 1981 return value == null ? Collections.<Object>emptyList() : Collections.singletonList(value); 1982 } 1983 1984 private SlpcObject statementSourceLocation(String artifactId, dataflow legacy) { 1985 List<String> coordinates = new ArrayList<String>(); 1986 for (table relation : dataflow.getAllTables(legacy)) { 1987 coordinates.add(relation.getCoordinate()); 1988 for (column field : relation.getColumns()) coordinates.add(field.getCoordinate()); 1989 } 1990 for (procedure routine : allProcedures(legacy)) { 1991 coordinates.add(routine.getCoordinate()); 1992 for (argument parameter : routine.getArguments()) coordinates.add(parameter.getCoordinate()); 1993 } 1994 for (gudusoft.gsqlparser.dlineage.dataflow.model.xml.process process : legacy.getProcesses()) { 1995 coordinates.add(process.getCoordinate()); 1996 } 1997 for (relationship relation : legacy.getRelationships()) { 1998 if (relation.getTarget() != null) coordinates.add(relation.getTarget().getCoordinate()); 1999 for (sourceColumn source : relation.getSources()) coordinates.add(source.getCoordinate()); 2000 for (sourceColumn callee : relation.getCallees()) coordinates.add(callee.getCoordinate()); 2001 } 2002 int startLine = Integer.MAX_VALUE, startColumn = Integer.MAX_VALUE, endLine = -1, endColumn = -1; 2003 for (String coordinate : coordinates) { 2004 int[] parsed = parsedCoordinate(coordinate); 2005 if (parsed == null) continue; 2006 if (parsed[0] < startLine || parsed[0] == startLine && parsed[1] < startColumn) { 2007 startLine = parsed[0]; startColumn = parsed[1]; 2008 } 2009 if (parsed[2] > endLine || parsed[2] == endLine && parsed[3] > endColumn) { 2010 endLine = parsed[2]; endColumn = parsed[3]; 2011 } 2012 } 2013 return startLine == Integer.MAX_VALUE ? null : sourceLocation(artifactId, startLine, startColumn, endLine, endColumn); 2014 } 2015 2016 private List<Object> sourceLocations(relationship relation, State state) { 2017 Map<String, SlpcObject> unique = new TreeMap<String, SlpcObject>(); 2018 if (relation.getTarget() != null) addSourceLocation(unique, relation.getTarget().getCoordinate(), state); 2019 for (sourceColumn source : relation.getSources()) addSourceLocation(unique, source.getCoordinate(), state); 2020 for (sourceColumn callee : relation.getCallees()) addSourceLocation(unique, callee.getCoordinate(), state); 2021 if (!unique.isEmpty()) state.hasSourceLocation = true; 2022 return new ArrayList<Object>(unique.values()); 2023 } 2024 2025 private List<Object> sourceLocations(String coordinate, State state) { 2026 Map<String, SlpcObject> unique = new TreeMap<String, SlpcObject>(); 2027 addSourceLocation(unique, coordinate, state); 2028 if (!unique.isEmpty()) state.hasSourceLocation = true; 2029 return new ArrayList<Object>(unique.values()); 2030 } 2031 2032 private void indexLegacySourceLocations(dataflow legacy, State state) { 2033 for (table relation : dataflow.getAllTables(legacy)) { 2034 state.sourceLocationsByLegacyEvidence.put("table:" + safeId(relation.getId(), relation.getName()), 2035 sourceLocations(relation.getCoordinate(), state)); 2036 for (column field : relation.getColumns()) { 2037 state.sourceLocationsByLegacyEvidence.put("column:" + safeId(field.getId(), field.getName()), 2038 sourceLocations(field.getCoordinate(), state)); 2039 } 2040 } 2041 for (relationship relation : legacy.getRelationships()) { 2042 state.sourceLocationsByLegacyEvidence.put(legacyId(relation), sourceLocations(relation, state)); 2043 } 2044 } 2045 2046 private void addSourceLocation(Map<String, SlpcObject> target, String coordinate, State state) { 2047 int[] parsed = parsedCoordinate(coordinate); 2048 if (parsed == null) return; 2049 SlpcObject value = sourceLocation(state.artifact.string("artifactId"), parsed[0], parsed[1], parsed[2], parsed[3]); 2050 target.put(parsed[0] + ":" + parsed[1] + ":" + parsed[2] + ":" + parsed[3], value); 2051 } 2052 2053 private int[] parsedCoordinate(String coordinate) { 2054 if (coordinate == null) return null; 2055 Matcher matcher = LEGACY_COORDINATE.matcher(coordinate); 2056 if (!matcher.find()) return null; 2057 int startLine, startColumn, startArtifact, endLine, endColumn, endArtifact; 2058 try { 2059 startLine = Integer.parseInt(matcher.group(1)); startColumn = Integer.parseInt(matcher.group(2)); 2060 startArtifact = Integer.parseInt(matcher.group(3)); endLine = Integer.parseInt(matcher.group(4)); 2061 endColumn = Integer.parseInt(matcher.group(5)); endArtifact = Integer.parseInt(matcher.group(6)); 2062 } catch (NumberFormatException invalid) { return null; } 2063 if (startLine < 1 || startColumn < 1 || endLine < 1 || endColumn < 1 2064 || startArtifact != 0 || endArtifact != 0 2065 || endLine < startLine || endLine == startLine && endColumn < startColumn) return null; 2066 return new int[] {startLine, startColumn, endLine, endColumn}; 2067 } 2068 2069 private SlpcObject sourceLocation(String artifactId, int startLine, int startColumn, int endLine, int endColumn) { 2070 return SlpcObject.builder().put("artifactId", artifactId).put("startLine", startLine) 2071 .put("startColumn", startColumn).put("endLine", endLine).put("endColumn", endColumn).build(); 2072 } 2073 2074 private List<Object> capabilities(State state) { 2075 List<Object> result = new ArrayList<Object>(); 2076 Set<String> partial = new LinkedHashSet<String>(); 2077 Collections.addAll(partial, "FACT_VALUE", "FACT_ROW_FILTER", "FACT_ROW_JOIN", "FACT_GROUP_BY", 2078 "TRANSFORM_EVIDENCE", "EXACT_IDENTIFIER_IDENTITY", "INTERMEDIATE_GRAPH", "JOIN_SEMANTICS", 2079 "STATEMENT_OPERATION", "LITERAL_PROVENANCE", "OBJECT_RELATIONSHIP", "FOREIGN_KEY_RELATIONSHIP", 2080 "PHYSICAL_HOP_COLLAPSE", "STORAGE_ASSET_IDENTITY", "ROUTINE_CALL", "STREAM_DEFINITION", 2081 "OBJECT_RENAME_SWAP_CLONE"); 2082 if (state.hasInteractiveExplain) partial.add("INTERACTIVE_EXPLAIN"); 2083 if (state.hasSourceLocation) partial.add("SOURCE_LOCATION"); 2084 if (state.hasRoutineBinding) { 2085 partial.add("SCOPED_OBJECTS"); partial.add("ROUTINE_OVERLOAD_IDENTITY"); 2086 } 2087 if (state.hasRoutineParameterFlow) partial.add("ROUTINE_PARAMETER_FLOW"); 2088 for (String code : SlpcSemanticValidator.CAPABILITIES) { 2089 String status = partial.contains(code) ? "PARTIAL" : "UNSUPPORTED"; 2090 if ("SECRET_REDACTION".equals(code)) status = "SUPPORTED"; 2091 result.add(SlpcObject.builder().put("code", code).put("status", status).build()); 2092 } 2093 state.diagnostics.add(diagnostic("SLPC_ADAPTER_CAPABILITY_GAP", "INFO", "DOCUMENT", null, 2094 "PARTIAL capabilities are bounded by completed legacy POJO evidence and caller-supplied identity/typed-transform proof; unsupported shapes remain candidates or diagnostics.")); 2095 return result; 2096 } 2097 2098 private SlpcObject producer(State state) { 2099 return SlpcObject.builder().put("engineKind", "LEGACY_DLINEAGE").put("engineVersion", state.context.engineVersion()) 2100 .put("adapterName", ADAPTER_NAME).put("adapterVersion", ADAPTER_VERSION).put("capabilities", capabilities(state)).build(); 2101 } 2102 2103 private void accountUnsupportedRelationship(relationship relation, State state) { 2104 accountLoss(relation, state, "SLPC_RELATIONSHIP_UNMAPPED", 2105 "Legacy relationship type '" + safe(relation.getType()) + "' has no proven v0.2 mapping in M1."); 2106 } 2107 2108 private void accountSemanticGap(relationship relation, String code, String message, State state) { 2109 String key = legacyId(relation) + "\u0000" + code; 2110 if (!state.semanticGapKeys.add(key)) return; 2111 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 2112 legacyId(relation), code, message)); 2113 state.diagnostics.add(diagnostic(code, "WARN", "DOCUMENT", null, message)); 2114 } 2115 2116 private void accountLoss(relationship relation, State state, String code, String message) { 2117 state.coverage.add(new LegacyCoverageReport.Entry("RELATIONSHIP", legacyId(relation), 2118 LegacyCoverageReport.Outcome.UNSUPPORTED_WITH_LOSS, null, code)); 2119 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 2120 legacyId(relation), code, message)); 2121 state.diagnostics.add(diagnostic(code, "WARN", "DOCUMENT", null, message)); 2122 } 2123 2124 private void accountErrors(dataflow legacy, State state) { 2125 int index = 0; 2126 for (gudusoft.gsqlparser.dlineage.dataflow.model.xml.error ignored : legacy.getErrors()) { 2127 index++; 2128 state.coverage.add(new LegacyCoverageReport.Entry("ERROR", "error-" + index, 2129 LegacyCoverageReport.Outcome.CONDITIONAL, null, "represented by document diagnostic")); 2130 state.diagnostics.add(diagnostic("GSP_LEGACY_ANALYSIS_ERROR", "WARN", "DOCUMENT", null, 2131 "The completed legacy analysis result contains an error record; its free text is intentionally not copied.")); 2132 } 2133 } 2134 2135 private int countLegacyRecords(dataflow legacy) { 2136 int count = legacy.getRelationships().size() + legacy.getErrors().size() + legacy.getProcesses().size(); 2137 for (table relation : dataflow.getAllTables(legacy)) count += 1 + relation.getColumns().size(); 2138 for (procedure routine : legacy.getProcedures()) count += 1 + routine.getArguments().size(); 2139 for (oraclePackage pkg : legacy.getPackages()) { 2140 count += 1 + pkg.getArguments().size(); 2141 for (procedure routine : pkg.getProcedures()) count += 1 + routine.getArguments().size(); 2142 } 2143 return count; 2144 } 2145 2146 private SlpcObject diagnostic(String code, String severity, String scope, String ref, String message) { 2147 return SlpcObject.builder().put("code", code).put("severity", severity).put("scope", scope).put("ref", ref) 2148 .put("message", message).putNull("sourceLocation").put("extensions", Collections.emptyList()).build(); 2149 } 2150 2151 private String resolveTarget(targetColumn target, FactSemantics semantics, State state) { 2152 if (target == null) return null; 2153 if ("ROW".equals(semantics.axis)) return relationEndpoint(target.getParent_id(), state); 2154 String result = state.endpointByLegacyId.get(target.getId()); 2155 if (result == null) result = state.endpointByLegacyId.get(target.getTarget_id()); 2156 return endpointAtValueLevel(result, state); 2157 } 2158 2159 private String resolveTargetField(targetColumn target, State state) { 2160 String result = state.endpointByLegacyId.get(target.getId()); 2161 if (result == null) result = state.endpointByLegacyId.get(target.getTarget_id()); 2162 return endpointAtLevel(result, "FIELD", state); 2163 } 2164 2165 private String resolveSource(sourceColumn source, FactSemantics semantics, State state) { 2166 String result = state.endpointByLegacyId.get(source.getId()); 2167 if (result == null) result = state.endpointByLegacyId.get(source.getSource_id()); 2168 result = endpointAtValueLevel(result, state); 2169 if (result == null && "system".equals(source.getSource()) 2170 && ("ROW".equals(semantics.axis) || "AGGREGATE".equals(semantics.role))) { 2171 result = relationEndpoint(source.getParent_id(), state); 2172 } 2173 return result; 2174 } 2175 2176 private String resolveSourceField(sourceColumn source, State state) { 2177 String result = state.endpointByLegacyId.get(source.getId()); 2178 if (result == null) result = state.endpointByLegacyId.get(source.getSource_id()); 2179 return endpointAtLevel(result, "FIELD", state); 2180 } 2181 2182 private String relationEndpoint(String legacyId, State state) { 2183 return endpointAtLevel(state.endpointByLegacyId.get(legacyId), "RELATION", state); 2184 } 2185 2186 private String relationEndpoint(targetColumn target, State state) { 2187 if (target == null) return null; 2188 String result = relationEndpoint(target.getParent_id(), state); 2189 if (result == null) result = relationEndpoint(target.getTarget_id(), state); 2190 if (result == null) result = relationEndpoint(target.getId(), state); 2191 return result; 2192 } 2193 2194 private String relationEndpoint(sourceColumn source, State state) { 2195 if (source == null) return null; 2196 String result = relationEndpoint(source.getParent_id(), state); 2197 if (result == null) result = relationEndpoint(source.getSource_id(), state); 2198 if (result == null) result = relationEndpoint(source.getId(), state); 2199 return result; 2200 } 2201 2202 private String endpointAtLevel(String ref, String level, State state) { 2203 SlpcObject endpoint = ref == null ? null : state.endpointByRef.get(ref); 2204 return endpoint != null && level.equals(endpoint.string("endpointLevel")) ? ref : null; 2205 } 2206 2207 private String endpointAtValueLevel(String ref, State state) { 2208 SlpcObject endpoint = ref == null ? null : state.endpointByRef.get(ref); 2209 return endpoint != null && equalsAny(endpoint.string("endpointLevel"), "FIELD", "VARIABLE") ? ref : null; 2210 } 2211 2212 private SlpcObject targetAnchor(targetColumn target, FactSemantics semantics, State state) { 2213 if (target == null) return null; 2214 boolean row = "ROW".equals(semantics.axis); 2215 NodeDraft node = row ? nodeByLegacyId(state, target.getParent_id()) 2216 : nodeByLegacyId(state, target.getId(), target.getTarget_id()); 2217 if (node != null) return nodeAnchor(node.finalRef, row ? "ROW" : "VALUE"); 2218 String endpoint = resolveTarget(target, semantics, state); 2219 return endpoint == null ? null : endpointAnchor(endpoint, row ? "ROW" : "VALUE"); 2220 } 2221 2222 private SlpcObject sourceAnchor(sourceColumn source, FactSemantics semantics, State state) { 2223 if (semantics.derived) return null; 2224 NodeDraft node = nodeByLegacyId(state, source.getId(), source.getSource_id()); 2225 if (node == null && "system".equals(source.getSource()) 2226 && ("ROW".equals(semantics.axis) || "AGGREGATE".equals(semantics.role))) { 2227 node = nodeByLegacyId(state, source.getParent_id()); 2228 } 2229 if (node != null) return nodeAnchor(node.finalRef, "ROW".equals(semantics.axis) ? "ROW" : "VALUE"); 2230 String endpoint = resolveSource(source, semantics, state); 2231 return endpoint == null ? null : endpointAnchor(endpoint, "ROW".equals(semantics.axis) ? "ROW" : "VALUE"); 2232 } 2233 2234 private NodeDraft nodeByLegacyId(State state, String... ids) { 2235 for (String id : ids) if (id != null && state.intermediateNodeByLegacyId.containsKey(id)) return state.intermediateNodeByLegacyId.get(id); 2236 return null; 2237 } 2238 2239 private SlpcObject endpointAnchor(String ref, String port) { return SlpcObject.builder().put("kind", "ENDPOINT").put("endpointRef", ref).put("portCode", port).build(); } 2240 private SlpcObject nodeAnchor(String ref, String port) { return SlpcObject.builder().put("kind", "NODE").put("nodeRef", ref).put("portCode", port).build(); } 2241 2242 private List<Object> displaySegments(table relation, column field) { 2243 List<Object> result = new ArrayList<Object>(); 2244 if (nonEmpty(relation.getServer()) != null) result.add(display("SERVER", relation.getServer())); 2245 if (nonEmpty(relation.getDatabase()) != null) result.add(display("DATABASE", relation.getDatabase())); 2246 if (nonEmpty(relation.getSchema()) != null) result.add(display("SCHEMA", relation.getSchema())); 2247 result.add(display("OBJECT", safe(relation.getName()))); 2248 if (field != null) result.add(display("COLUMN", safe(field.getName()))); 2249 return result; 2250 } 2251 2252 private SlpcObject display(String kind, String value) { 2253 return SlpcObject.builder().put("kind", kind).put("display", value) 2254 .put("inputForm", "ENGINE_SYNTHETIC").put("provenance", "ENGINE_SYNTHETIC").build(); 2255 } 2256 2257 private String objectKind(table relation, State state) { 2258 if (relation.isResultSet()) { 2259 String type = safe(relation.getType()); 2260 ResultSetType resultSetType = ResultSetType.of(type); 2261 if (resultSetType == ResultSetType.cte || "with_cte".equals(type)) return "CTE"; 2262 if (resultSetType == ResultSetType.result_of) return "DERIVED_TABLE"; 2263 return "RESULT_SET"; 2264 } 2265 if (relation.isView()) return "VIEW"; 2266 if (relation.isStream()) return "STREAM"; 2267 if (relation.isStage()) return "STAGE"; 2268 if (relation.isFile()) return "FILE"; 2269 if (relation.isCursor()) return "CURSOR"; 2270 if (relation.isVariable()) return "SCALAR_VARIABLE"; 2271 if ("procedure".equalsIgnoreCase(relation.getType())) return "PROCEDURE"; 2272 if ("CREATE_EXTERNAL_TABLE".equals(operationKind(state.context.statementKind())) 2273 && (relation.isTarget() || createdInSql(relation))) return "EXTERNAL_TABLE"; 2274 if ("CREATE_MATERIALIZED_VIEW".equals(operationKind(state.context.statementKind())) 2275 && (relation.isTarget() || createdInSql(relation))) return "MATERIALIZED_VIEW"; 2276 if (relation.isTable()) return "TABLE"; 2277 return "UNKNOWN"; 2278 } 2279 2280 private String routineObjectKind(table relation) { 2281 return "function".equalsIgnoreCase(relation.getType()) ? "FUNCTION" : "PROCEDURE"; 2282 } 2283 2284 private String endpointLevel(table relation, String legacyKind) { 2285 if ("PROCEDURE".equals(legacyKind)) return "ROUTINE"; 2286 if (relation.isVariable() || relation.isCursor()) return "VARIABLE"; 2287 return "RELATION"; 2288 } 2289 2290 private String nodeKind(table relation) { 2291 String raw = safe(relation.getType()); 2292 if ("with_cte".equals(raw)) return "CTE"; 2293 ResultSetType type = ResultSetType.of(raw); 2294 if (type == null) return "OTHER"; 2295 switch (type) { 2296 case select_list: return "SELECT_LIST"; 2297 case cte: return "CTE"; 2298 case insert_select: return "INSERT_SELECT"; 2299 case update_select: return "UPDATE_SELECT"; 2300 case update_set: return "UPDATE_SET"; 2301 case merge_update: return "MERGE_UPDATE"; 2302 case merge_insert: return "MERGE_INSERT"; 2303 case output: return "OUTPUT"; 2304 case pivot_table: return "PIVOT"; 2305 case unpivot_table: return "UNPIVOT"; 2306 case alias: return "ALIAS"; 2307 case function: return "FUNCTION_RESULT"; 2308 case array: return "ARRAY"; 2309 case struct: return "STRUCT"; 2310 case result_of: return "RESULT_OF"; 2311 case variable: return "VARIABLE"; 2312 default: return "OTHER"; 2313 } 2314 } 2315 2316 private String nodeKindForField(table relation) { 2317 // A projected field is not automatically a SQL alias. The legacy 2318 // column POJO has no dedicated alias discriminator after the default 2319 // finish path, so retain the node without inventing ALIAS semantics. 2320 return "OTHER"; 2321 } 2322 2323 private boolean isIntermediate(table relation) { 2324 return relation.isResultSet(); 2325 } 2326 2327 private boolean isRelationRows(column field) { 2328 return "system".equals(field.getSource()) || "RelationRows".equals(field.getName()); 2329 } 2330 2331 private boolean hasCandidateTables(table relation) { 2332 return relation.getCandidateTables() != null && !relation.getCandidateTables().isEmpty(); 2333 } 2334 2335 private boolean hasCandidateParents(sourceColumn source) { 2336 return source.getCandidateParents() != null && !source.getCandidateParents().isEmpty(); 2337 } 2338 2339 /** 2340 * These legacy effect codes describe object/storage operations or an 2341 * assumed dependency. Treating them as ordinary VALUE/ROW facts would be 2342 * a semantic invention; a later M1 slice must map them to the corresponding 2343 * statement operation, object relationship, or quality model. 2344 */ 2345 private boolean unsupportedFactEffect(String value) { 2346 return equalsAny(value, 2347 "rename_table", "swap_table", "append_from", "like_table", 2348 "clone_database", "clone_schema", "clone_table", "unload", "copy", 2349 "foreign_key", "create_synonym", "exchange_partition", "synonym", 2350 "drop_table", "drop_table_column", "add_table_column", "truncate_table", 2351 "external_script_passthrough"); 2352 } 2353 2354 /** Data dependencies remain useful, but a second typed operation is owed. */ 2355 private boolean effectNeedsAdditionalMapping(String value) { 2356 return equalsAny(value, "create_view", "create_table", "trigger", "expand_star"); 2357 } 2358 2359 private boolean hasUnavailableIdentity(List<EndpointDraft> drafts) { 2360 for (EndpointDraft draft : drafts) if (draft.canonical == null) return true; 2361 return false; 2362 } 2363 2364 /** Only a dedicated alias field is accepted as SQL-proven local naming evidence. */ 2365 private String provenLocalName(table relation) { 2366 return nonEmpty(relation.getAlias()); 2367 } 2368 2369 private void containmentLoss(table relation, State state, String message) { 2370 String owner = safeId(relation.getId(), safe(relation.getName())); 2371 state.differences.add(new IntentionalDiffLedger.Entry(IntentionalDiffLedger.Category.KNOWN_UNSUPPORTED_WITH_LOSS, 2372 owner, "GSP_LEGACY_CONTAINMENT_UNRESOLVED", message)); 2373 } 2374 2375 private void breakContainmentCycles(List<table> intermediate, IdentityHashMap<table, table> parentByTable, State state) { 2376 Set<table> settled = Collections.newSetFromMap(new IdentityHashMap<table, Boolean>()); 2377 for (table start : intermediate) { 2378 if (settled.contains(start)) continue; 2379 List<table> path = new ArrayList<table>(); 2380 IdentityHashMap<table, Integer> position = new IdentityHashMap<table, Integer>(); 2381 table current = start; 2382 while (current != null && !settled.contains(current)) { 2383 Integer repeatedAt = position.get(current); 2384 if (repeatedAt != null) { 2385 List<table> cycle = new ArrayList<table>(path.subList(repeatedAt, path.size())); 2386 Collections.sort(cycle, tableComparator()); 2387 table detached = cycle.get(0); 2388 parentByTable.remove(detached); 2389 containmentLoss(detached, state, 2390 "Legacy intermediate containment contains a cycle; one deterministic link was removed and the node retained as a root."); 2391 break; 2392 } 2393 position.put(current, path.size()); path.add(current); current = parentByTable.get(current); 2394 } 2395 settled.addAll(path); 2396 } 2397 } 2398 2399 private Comparator<table> sourceTableComparator() { 2400 return new Comparator<table>() { @Override public int compare(table left, table right) { 2401 int coordinate = compareCoordinates(left.getCoordinate(), right.getCoordinate()); 2402 return coordinate != 0 ? coordinate : tableComparator().compare(left, right); 2403 }}; 2404 } 2405 2406 private Comparator<PendingNode> pendingNodeComparator() { 2407 return new Comparator<PendingNode>() { @Override public int compare(PendingNode left, PendingNode right) { 2408 String leftCoordinate = left.field == null ? left.relation.getCoordinate() : left.field.getCoordinate(); 2409 String rightCoordinate = right.field == null ? right.relation.getCoordinate() : right.field.getCoordinate(); 2410 int coordinate = compareCoordinates(leftCoordinate, rightCoordinate); 2411 if (coordinate != 0) return coordinate; 2412 String leftKey = (left.field == null ? "0" : "1") + "\u0000" + safe(left.relation.getId()) + "\u0000" 2413 + safe(left.field == null ? null : left.field.getId()) + "\u0000" + safe(left.relation.getName()) + "\u0000" 2414 + safe(left.field == null ? null : left.field.getName()); 2415 String rightKey = (right.field == null ? "0" : "1") + "\u0000" + safe(right.relation.getId()) + "\u0000" 2416 + safe(right.field == null ? null : right.field.getId()) + "\u0000" + safe(right.relation.getName()) + "\u0000" 2417 + safe(right.field == null ? null : right.field.getName()); 2418 return utf8Compare(leftKey, rightKey); 2419 }}; 2420 } 2421 2422 private int compareCoordinates(String left, String right) { 2423 long[] a = firstCoordinate(left), b = firstCoordinate(right); 2424 for (int index = 0; index < a.length; index++) { 2425 if (a[index] < b[index]) return -1; 2426 if (a[index] > b[index]) return 1; 2427 } 2428 return 0; 2429 } 2430 2431 private long[] firstCoordinate(String value) { 2432 long unavailable = Long.MAX_VALUE; 2433 long[] result = {unavailable, unavailable, unavailable}; 2434 if (value == null) return result; 2435 int open = value.indexOf('['), close = value.indexOf(']', open + 1); 2436 if (open < 0 || close < 0) return result; 2437 String[] parts = value.substring(open + 1, close).split(",", -1); 2438 for (int index = 0; index < result.length && index < parts.length; index++) { 2439 try { 2440 long parsed = Long.parseLong(parts[index].trim()); 2441 result[index] = parsed < 0 ? unavailable : parsed; 2442 } catch (NumberFormatException ignored) { 2443 result[index] = unavailable; 2444 } 2445 } 2446 return result; 2447 } 2448 2449 private void putNodeLegacyId(Map<String, NodeDraft> target, String id, NodeDraft node) { 2450 if (id == null) return; 2451 NodeDraft previous = target.put(id, node); 2452 if (previous != null && previous != node) throw new IllegalStateException("duplicate legacy intermediate identifier: " + id); 2453 } 2454 2455 private boolean isCanonicalFact(SlpcObject semantic, Map<String, SlpcObject> endpoints) { 2456 String target = semantic.string("targetEndpointRef"); if (endpoints.get(target).string("canonicalEndpointId") == null) return false; 2457 String source = semantic.string("sourceEndpointRef"); return source == null || endpoints.get(source).string("canonicalEndpointId") != null; 2458 } 2459 2460 private boolean isCanonicalIdentity(SlpcObject identity) { 2461 String kind = identity.string("kind"); return "COMPOSITE_EXACT".equals(kind) || "AUTHORITATIVE_EXTERNAL".equals(kind) || "URI_EXACT".equals(kind); 2462 } 2463 2464 private String candidateReason(SlpcObject semantic) { 2465 return "IDENTITY_UNAVAILABLE"; 2466 } 2467 2468 private SlpcObject occurrenceQuality(String resolution) { 2469 return quality(resolution, "EXACT", "PROVEN", 1000000); 2470 } 2471 2472 private SlpcObject quality(String resolution, String evaluation, String proof, int confidence) { 2473 return SlpcObject.builder().put("resolutionGrade", resolution).put("evaluationModel", evaluation) 2474 .put("semanticProof", proof).put("dynamic", false).put("confidenceMicros", confidence).build(); 2475 } 2476 2477 private SlpcObject redactedLiteral(State state, int redactionOrdinal) { 2478 String kind = "UNKNOWN"; 2479 SlpcObject fingerprint = SlpcObject.builder().put("algorithm", "SLPC-REDACTED-LITERAL-V1") 2480 .put("value", SlpcIdentity.redactedLiteralHash(state.context.sqlDialect(), kind, redactionOrdinal)).build(); 2481 return SlpcObject.builder().put("inputOrdinal", 1).put("literalKind", kind) 2482 .put("text", "<SLPC-REDACTED:" + kind + ":" + redactionOrdinal + ">") 2483 .put("redactionOrdinal", redactionOrdinal).put("fingerprint", fingerprint) 2484 .putNull("sourceLocation").put("locationNullReason", "UNKNOWN").build(); 2485 } 2486 2487 private SlpcObject qualitySummary(String eligibility, String resolution, int count) { 2488 return SlpcObject.builder().put("projectionEligibility", eligibility).put("resolutionGrade", resolution) 2489 .put("evaluationModel", "EXACT").put("semanticProof", "PROVEN").put("dynamic", false) 2490 .put("confidenceMicros", 1000000).put("evidenceCount", count).build(); 2491 } 2492 2493 private boolean allCanonical(List<String> keys, State state) { for (String key : keys) if (!state.factGroups.get(key).canonical) return false; return true; } 2494 2495 private static Comparator<FactGroup> factGroupComparator() { return new Comparator<FactGroup>() { @Override public int compare(FactGroup left, FactGroup right) { return left.semanticKey.compareTo(right.semanticKey); }}; } 2496 private static Comparator<RelationshipGroup> relationshipGroupComparator() { return new Comparator<RelationshipGroup>() { @Override public int compare(RelationshipGroup left, RelationshipGroup right) { return left.semanticKey.compareTo(right.semanticKey); }}; } 2497 private static Comparator<NodeDraft> nodeFingerprintComparator() { return new Comparator<NodeDraft>() { @Override public int compare(NodeDraft left, NodeDraft right) { return left.fingerprint.compareTo(right.fingerprint); }}; } 2498 private static Comparator<EdgeDraft> edgeFingerprintComparator() { return new Comparator<EdgeDraft>() { @Override public int compare(EdgeDraft left, EdgeDraft right) { return left.fingerprint.compareTo(right.fingerprint); }}; } 2499 private static Comparator<PathDraft> pathFingerprintComparator() { return new Comparator<PathDraft>() { @Override public int compare(PathDraft left, PathDraft right) { return left.fingerprint.compareTo(right.fingerprint); }}; } 2500 private static Comparator<table> tableComparator() { return new Comparator<table>() { @Override public int compare(table left, table right) { 2501 return utf8Compare(safe(left.getId()) + "\u0000" + safe(left.getName()), 2502 safe(right.getId()) + "\u0000" + safe(right.getName())); 2503 }}; } 2504 2505 private static void putLegacyRef(Map<String, String> refs, String id, String ref) { if (id != null && !refs.containsKey(id)) refs.put(id, ref); } 2506 private static String legacyId(relationship relation) { return safeId(relation.getId(), "relationship:" + safe(relation.getType())); } 2507 private static String safeId(String id, String fallback) { return id == null || id.isEmpty() ? fallback : id; } 2508 private static String safe(String value) { return value == null ? "" : value; } 2509 private static String nonEmpty(String value) { return value == null || value.isEmpty() ? null : value; } 2510 private static String local(String prefix, int ordinal) { return String.format("%s%06d", prefix, ordinal); } 2511 private static String zeros(int count) { StringBuilder result = new StringBuilder(); for (int index = 0; index < count; index++) result.append('0'); return result.toString(); } 2512 private static String join(List<String> values) { StringBuilder result = new StringBuilder(); for (String value : values) { if (result.length() > 0) result.append(','); result.append(value); } return result.toString(); } 2513 private static boolean equalsAny(String value, String... choices) { if (value == null) return false; for (String choice : choices) if (value.equals(choice)) return true; return false; } 2514 private static boolean equalValues(Object left, Object right) { return left == null ? right == null : left.equals(right); } 2515 private static int utf8Compare(String left, String right) { byte[] a = left.getBytes(StandardCharsets.UTF_8), b = right.getBytes(StandardCharsets.UTF_8); int common = Math.min(a.length, b.length); for (int index = 0; index < common; index++) { int difference = (a[index] & 255) - (b[index] & 255); if (difference != 0) return difference; } return a.length - b.length; } 2516 private static String sha256(byte[] value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value); StringBuilder result = new StringBuilder(); for (byte item : digest) result.append(String.format("%02x", item & 255)); return result.toString(); } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); } } 2517 2518 private final class State { 2519 private final LegacyAdapterContext context; 2520 private SlpcObject artifact, statement; private String statementId; 2521 private final List<Object> endpoints = new ArrayList<Object>(), facts = new ArrayList<Object>(), factCandidates = new ArrayList<Object>(), occurrences = new ArrayList<Object>(), diagnostics = new ArrayList<Object>(), graphs = new ArrayList<Object>(), statementOperations = new ArrayList<Object>(), joinSemantics = new ArrayList<Object>(), objectRelationships = new ArrayList<Object>(), objectRelationshipCandidates = new ArrayList<Object>(); 2522 private final Map<String, SlpcObject> endpointByRef = new LinkedHashMap<String, SlpcObject>(); 2523 private final Map<String, String> endpointByLegacyId = new HashMap<String, String>(); 2524 private final IdentityHashMap<table, EndpointDraft> tableDrafts = new IdentityHashMap<table, EndpointDraft>(); 2525 private final IdentityHashMap<column, EndpointDraft> columnDrafts = new IdentityHashMap<column, EndpointDraft>(); 2526 private final IdentityHashMap<procedure, EndpointDraft> routineDrafts = new IdentityHashMap<procedure, EndpointDraft>(); 2527 private final IdentityHashMap<procedure, LegacyAdapterContext.RoutineBinding> routineBindings = 2528 new IdentityHashMap<procedure, LegacyAdapterContext.RoutineBinding>(); 2529 private final IdentityHashMap<procedure, Map<String, EndpointDraft>> parameterDraftsByRoutine = 2530 new IdentityHashMap<procedure, Map<String, EndpointDraft>>(); 2531 private final IdentityHashMap<EndpointDraft, LegacyAdapterContext.RoutineParameterBinding> parameterBindingByDraft = 2532 new IdentityHashMap<EndpointDraft, LegacyAdapterContext.RoutineParameterBinding>(); 2533 private final Map<String, String> parameterRefByKey = new HashMap<String, String>(); 2534 private final Map<String, List<Object>> sourceLocationsByLegacyEvidence = 2535 new HashMap<String, List<Object>>(); 2536 private final Set<String> mappedArgumentIds = new HashSet<String>(); 2537 private final IdentityHashMap<table, String> tableRefs = new IdentityHashMap<table, String>(); 2538 private final IdentityHashMap<column, String> columnRefs = new IdentityHashMap<column, String>(); 2539 private final IdentityHashMap<table, NodeDraft> intermediateTableNodes = new IdentityHashMap<table, NodeDraft>(); 2540 private final IdentityHashMap<column, NodeDraft> intermediateColumnNodes = new IdentityHashMap<column, NodeDraft>(); 2541 private final Map<String, NodeDraft> intermediateNodeByLegacyId = new HashMap<String, NodeDraft>(); 2542 private final IdentityHashMap<table, String> intermediateTableRefs = new IdentityHashMap<table, String>(); 2543 private final IdentityHashMap<column, String> intermediateColumnRefs = new IdentityHashMap<column, String>(); 2544 private final Map<String, FactGroup> factGroups = new LinkedHashMap<String, FactGroup>(); 2545 private final Map<String, RelationshipGroup> relationshipGroups = new LinkedHashMap<String, RelationshipGroup>(); 2546 private final List<FactOccurrenceDraft> factOccurrenceDrafts = new ArrayList<FactOccurrenceDraft>(); 2547 private final Set<String> dedicatedRelationshipAccounting = new HashSet<String>(); 2548 private int nextRedactionOrdinal = 1; 2549 private int legacyInputCount; 2550 private boolean hasLegacyEndpointAmbiguity; 2551 private boolean hasStorageIdentityUnavailable, hasPhysicalHopCollapse, hasInteractiveExplain; 2552 private boolean hasRoutineBinding, hasRoutineParameterFlow, hasSourceLocation; 2553 private boolean pathCycleObserved, pathEnumerationTruncated; 2554 private final List<LegacyCoverageReport.Entry> coverage = new ArrayList<LegacyCoverageReport.Entry>(); 2555 private final List<IntentionalDiffLedger.Entry> differences = new ArrayList<IntentionalDiffLedger.Entry>(); 2556 private final Set<String> semanticGapKeys = new HashSet<String>(); 2557 private State(LegacyAdapterContext context) { this.context = context; } 2558 2559 private LegacyAdaptation finish() { 2560 SlpcObject root = SlpcDocument.rootBuilder().put("producer", producer(this)) 2561 .put("artifacts", Collections.singletonList(artifact)).put("statements", Collections.singletonList(statement)) 2562 .put("endpoints", endpoints).put("facts", facts).put("factCandidates", factCandidates) 2563 .put("datasetOperationInfluences", Collections.emptyList()).put("datasetOperationCandidates", Collections.emptyList()) 2564 .put("occurrences", occurrences).put("diagnostics", diagnostics).put("extensions", Collections.emptyList()) 2565 .put("objectRelationships", objectRelationships).put("objectRelationshipCandidates", objectRelationshipCandidates) 2566 .put("statementOperations", statementOperations).put("joinSemantics", joinSemantics) 2567 .put("intermediateGraphs", graphs).build(); 2568 return new LegacyAdaptation(new SlpcDocument(root), new LegacyCoverageReport(legacyInputCount, coverage), new IntentionalDiffLedger(differences)); 2569 } 2570 } 2571 2572 private static final class EndpointDraft { 2573 private final table relation; private final column field; private final SlpcObject provisional; private final String canonical, sortKey, stableTieBreaker, legacyKind, legacyId; private String endpointRef; private EndpointDraft representative; private procedure routine; 2574 private EndpointDraft(table relation, column field, SlpcObject provisional, String canonical, String sortKey, String stableTieBreaker, String legacyKind, String legacyId) { this.relation = relation; this.field = field; this.provisional = provisional; this.canonical = canonical; this.sortKey = sortKey; this.stableTieBreaker = stableTieBreaker; this.legacyKind = legacyKind; this.legacyId = legacyId; } 2575 private SlpcObject withRef() { return provisional.toBuilder().put("endpointRef", endpointRef).build(); } 2576 private boolean sameSemanticIdentity(EndpointDraft other) { 2577 if (canonical != null || other.canonical != null) { 2578 return canonical != null && canonical.equals(other.canonical) 2579 && provisional.string("endpointLevel").equals(other.provisional.string("endpointLevel")) 2580 && provisional.string("objectKind").equals(other.provisional.string("objectKind")) 2581 && provisional.get("identity").equals(other.provisional.get("identity")) 2582 && equalValues(provisional.get("scope"), other.provisional.get("scope")); 2583 } 2584 return provisional.toBuilder().remove("endpointRef").build() 2585 .equals(other.provisional.toBuilder().remove("endpointRef").build()); 2586 } 2587 } 2588 2589 private static final class FactSemantics { 2590 private final String axis, role, operationKind, operationInputKind, diagnosticCode, diagnosticMessage; 2591 private final boolean derived, publish; 2592 private FactSemantics(String axis, String role, String operationKind, String operationInputKind, boolean derived, 2593 boolean publish, String diagnosticCode, String diagnosticMessage) { 2594 this.axis = axis; this.role = role; this.operationKind = operationKind; this.operationInputKind = operationInputKind; 2595 this.derived = derived; this.publish = publish; this.diagnosticCode = diagnosticCode; this.diagnosticMessage = diagnosticMessage; 2596 } 2597 private static FactSemantics publish(String axis, String role, String operationKind, String operationInputKind, boolean derived) { 2598 return new FactSemantics(axis, role, operationKind, operationInputKind, derived, true, null, null); 2599 } 2600 private static FactSemantics publishWithGap(String axis, String role, String operationKind, String operationInputKind, 2601 boolean derived, String diagnosticCode, String diagnosticMessage) { 2602 return new FactSemantics(axis, role, operationKind, operationInputKind, derived, true, diagnosticCode, diagnosticMessage); 2603 } 2604 private static FactSemantics reject(String diagnosticCode, String diagnosticMessage) { 2605 return new FactSemantics("ROW", "UNKNOWN", "NONE", "NONE", false, false, diagnosticCode, diagnosticMessage); 2606 } 2607 } 2608 2609 private static final class SystemFieldDraft { 2610 private final table relation; private final column field; 2611 private SystemFieldDraft(table relation, column field) { this.relation = relation; this.field = field; } 2612 } 2613 2614 private static final class PendingNode { 2615 private final table relation; private final column field; private final NodeDraft parent; 2616 private PendingNode(table relation, column field, NodeDraft parent) { this.relation = relation; this.field = field; this.parent = parent; } 2617 } 2618 2619 private static final class FactGroup { 2620 private final SlpcObject semantic; private final boolean canonical; private final String semanticKey; private final List<FactOccurrenceDraft> occurrences = new ArrayList<FactOccurrenceDraft>(); private String ownerKind, ownerId; 2621 private FactGroup(SlpcObject semantic, boolean canonical, String semanticKey) { this.semantic = semantic; this.canonical = canonical; this.semanticKey = semanticKey; } 2622 private boolean sameSemantic(SlpcObject other) { return semantic.equals(other); } 2623 } 2624 2625 private static final class FactOccurrenceDraft { 2626 private final FactGroup group; private final SlpcObject transformation, predicate; private final String resolution; 2627 private final List<Object> literalInputs, supportPathRefs, sourceLocations; private final String procedureEndpointRef; 2628 private FactOccurrenceDraft(FactGroup group, SlpcObject transformation, SlpcObject predicate, String resolution, 2629 List<Object> literalInputs, List<Object> supportPathRefs) { 2630 this(group, transformation, predicate, resolution, literalInputs, supportPathRefs, 2631 Collections.<Object>emptyList(), null); 2632 } 2633 private FactOccurrenceDraft(FactGroup group, SlpcObject transformation, SlpcObject predicate, String resolution, 2634 List<Object> literalInputs, List<Object> supportPathRefs, List<Object> sourceLocations, 2635 String procedureEndpointRef) { 2636 this.group = group; this.transformation = transformation; this.predicate = predicate; 2637 this.resolution = resolution; this.literalInputs = literalInputs; this.supportPathRefs = supportPathRefs; 2638 this.sourceLocations = sourceLocations; this.procedureEndpointRef = procedureEndpointRef; 2639 } 2640 } 2641 2642 private static final class RelationshipGroup { 2643 private final SlpcObject semantic; private final boolean canonical; private final String semanticKey; 2644 private final Set<String> legacyEvidence = new LinkedHashSet<String>(); private String ownerKind, ownerId; 2645 private final Map<String, SlpcObject> sourceLocations = new TreeMap<String, SlpcObject>(); 2646 private RelationshipGroup(SlpcObject semantic, boolean canonical, String semanticKey) { 2647 this.semantic = semantic; this.canonical = canonical; this.semanticKey = semanticKey; 2648 } 2649 } 2650 2651 private static final class NodeDraft { 2652 private final table relation; private final column field; private final int ordinal; private final String kind, localName; private final NodeDraft parent; 2653 private String provisionalRef, finalRef, fingerprint; private SlpcObject provisionalNode; 2654 private NodeDraft(table relation, column field, int ordinal, String kind, String localName, NodeDraft parent) { this.relation = relation; this.field = field; this.ordinal = ordinal; this.kind = kind; this.localName = localName; this.parent = parent; } 2655 private SlpcObject node(String ref, String parentRef, String fingerprint) { return SlpcObject.builder().put("nodeRef", ref).put("nodeFingerprint", fingerprint).put("nodeOrdinal", ordinal).put("nodeKind", kind).put("localName", localName).put("parentNodeRef", parentRef).putNull("boundaryEndpointRef").putNull("nodeEvidence").put("extensions", Collections.emptyList()).build(); } 2656 } 2657 2658 private static final class EdgeDraft { 2659 private final relationship relation; private final sourceColumn sourceColumn; 2660 private final SlpcObject source, target; private final FactSemantics semantics; private final SlpcObject transformation, predicate; private String fingerprint, finalRef; 2661 private EdgeDraft(relationship relation, sourceColumn sourceColumn, SlpcObject source, SlpcObject target, 2662 FactSemantics semantics, SlpcObject transformation, SlpcObject predicate) { this.relation = relation; this.sourceColumn = sourceColumn; this.source = source; this.target = target; this.semantics = semantics; this.transformation = transformation; this.predicate = predicate; } 2663 private SlpcObject edge(String ref, String fingerprint) { return SlpcObject.builder().put("source", source).put("target", target).put("edgeRef", ref).put("edgeFingerprint", fingerprint).put("axis", semantics.axis).put("role", semantics.role).put("operationKind", semantics.operationKind).put("operationInputKind", semantics.operationInputKind).put("transformation", transformation).put("predicate", predicate).put("extensions", Collections.emptyList()).build(); } 2664 private boolean sameSemantic(EdgeDraft other) { return source.equals(other.source) && target.equals(other.target) 2665 && semantics.axis.equals(other.semantics.axis) && semantics.role.equals(other.semantics.role) 2666 && semantics.operationKind.equals(other.semantics.operationKind) 2667 && semantics.operationInputKind.equals(other.semantics.operationInputKind) 2668 && (transformation == null ? other.transformation == null : transformation.equals(other.transformation)) 2669 && (predicate == null ? other.predicate == null : predicate.equals(other.predicate)); } 2670 } 2671 2672 private static final class PathDraft { 2673 private final List<EdgeDraft> edges; private String fingerprint, finalRef; 2674 private PathDraft(List<EdgeDraft> edges) { this.edges = new ArrayList<EdgeDraft>(edges); } 2675 private SlpcObject path(String ref, String fp) { 2676 List<Object> refs = new ArrayList<Object>(); for (EdgeDraft edge : edges) refs.add(edge.finalRef); 2677 return SlpcObject.builder().put("pathRef", ref).put("pathFingerprint", fp) 2678 .put("edgeRefs", refs).put("extensions", Collections.emptyList()).build(); 2679 } 2680 private String sourceEndpointRef() { return "ENDPOINT".equals(edges.get(0).source.string("kind")) 2681 ? edges.get(0).source.string("endpointRef") : null; } 2682 private String targetEndpointRef() { EdgeDraft last = edges.get(edges.size() - 1); return "ENDPOINT".equals(last.target.string("kind")) 2683 ? last.target.string("endpointRef") : null; } 2684 private String axis() { return edges.get(0).semantics.axis; } 2685 } 2686 2687 private static final class PathWalk { 2688 private final List<EdgeDraft> edges; private final Set<String> used; 2689 private PathWalk(List<EdgeDraft> edges, Set<String> used) { this.edges = edges; this.used = used; } 2690 private static PathWalk start(EdgeDraft edge) { 2691 List<EdgeDraft> edges = new ArrayList<EdgeDraft>(); edges.add(edge); 2692 Set<String> used = new HashSet<String>(); used.add(edge.finalRef); return new PathWalk(edges, used); 2693 } 2694 private PathWalk append(EdgeDraft edge) { 2695 List<EdgeDraft> copy = new ArrayList<EdgeDraft>(edges); copy.add(edge); 2696 Set<String> copyUsed = new HashSet<String>(used); copyUsed.add(edge.finalRef); return new PathWalk(copy, copyUsed); 2697 } 2698 private String edgeKey() { StringBuilder result = new StringBuilder(); for (EdgeDraft edge : edges) result.append(edge.finalRef).append('\u0000'); return result.toString(); } 2699 } 2700 2701 private static final class StepDraft { 2702 private final String family, code; private final List<Object> bindings; private final SlpcObject result, evidence; 2703 private StepDraft(String family, String code, List<Object> bindings, SlpcObject result, SlpcObject evidence) { 2704 this.family = family; this.code = code; this.bindings = bindings; this.result = result; this.evidence = evidence; 2705 } 2706 private SlpcObject step(String ref, String fingerprint, int ordinal) { 2707 return SlpcObject.builder().put("stepRef", ref).put("stepFingerprint", fingerprint) 2708 .put("stepOrdinal", ordinal).put("operationFamily", family).put("operationCode", code) 2709 .put("inputBindings", bindings).put("resultAnchor", result).put("expressionEvidence", evidence) 2710 .put("extensions", Collections.emptyList()).build(); 2711 } 2712 private String sortKey() { 2713 StringBuilder resultKey = new StringBuilder(family).append('\u0000').append(code).append('\u0000'); 2714 for (Object value : bindings) resultKey.append(((SlpcObject) value).string("edgeRef")).append('\u0000'); 2715 return resultKey.toString(); 2716 } 2717 } 2718}