001package gudusoft.gsqlparser.slpc.adapter; 002 003import gudusoft.gsqlparser.TBaseType; 004import gudusoft.gsqlparser.dlineage.dataflow.model.xml.column; 005import gudusoft.gsqlparser.dlineage.dataflow.model.AuthoritativeLineageEvidence; 006import gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow; 007import gudusoft.gsqlparser.dlineage.dataflow.model.xml.procedure; 008import gudusoft.gsqlparser.dlineage.dataflow.model.xml.relationship; 009import gudusoft.gsqlparser.dlineage.dataflow.model.xml.table; 010import gudusoft.gsqlparser.slpc.model.SlpcObject; 011 012import java.nio.charset.StandardCharsets; 013import java.security.MessageDigest; 014import java.security.NoSuchAlgorithmException; 015import java.util.ArrayList; 016import java.util.Collections; 017import java.util.List; 018 019/** 020 * Caller-supplied facts that do not exist in the legacy dataflow POJO. 021 * Supplying these values is not SQL reparsing; it makes artifact and identity 022 * provenance explicit at the adapter boundary. 023 */ 024public final class LegacyAdapterContext { 025 026 public interface IdentityResolver { 027 /** Returns a proven descriptor or null to quarantine the endpoint. */ 028 ResolvedIdentity resolve(table relation, column field); 029 } 030 031 public static final IdentityResolver NO_IDENTITY = new IdentityResolver() { 032 @Override public ResolvedIdentity resolve(table relation, column field) { return null; } 033 }; 034 035 /** 036 * Supplies typed operation and ordered-operand evidence produced while the 037 * SQL AST is still available. The legacy XML transform text and its set 038 * iteration order are deliberately not treated as this evidence. 039 */ 040 public interface TypedTransformResolver { 041 /** Returns a proven profile for this legacy relationship, or null. */ 042 TypedTransform resolve(relationship relation); 043 } 044 045 public static final TypedTransformResolver NO_TYPED_TRANSFORM = new TypedTransformResolver() { 046 @Override public TypedTransform resolve(relationship relation) { return null; } 047 }; 048 049 /** 050 * Supplies overload identity, formal-parameter binding, and explicit 051 * parameter/return flows while the routine AST or catalog signature is 052 * still available. Legacy argument names are display evidence only and 053 * are never matched to variable names by this adapter. 054 */ 055 public interface RoutineBindingResolver { 056 RoutineBinding resolve(procedure routine); 057 } 058 059 public static final RoutineBindingResolver NO_ROUTINE_BINDING = new RoutineBindingResolver() { 060 @Override public RoutineBinding resolve(procedure routine) { return null; } 061 }; 062 063 /** Supplies declaration-order composite FK pairs lost by legacy output. */ 064 public interface CompositeForeignKeyResolver { 065 List<ForeignKeyBinding> resolve(dataflow completed); 066 } 067 068 public static final CompositeForeignKeyResolver NO_COMPOSITE_FOREIGN_KEY = 069 new CompositeForeignKeyResolver() { 070 @Override public List<ForeignKeyBinding> resolve(dataflow completed) { 071 return Collections.emptyList(); 072 } 073 }; 074 075 private final String sourceKind; 076 private final String logicalLocator; 077 private final String sqlDialect; 078 private final String contentSha256; 079 private final String statementKind; 080 private final String engineVersion; 081 private final IdentityResolver identityResolver; 082 private final TypedTransformResolver typedTransformResolver; 083 private final RoutineBindingResolver routineBindingResolver; 084 private final CompositeForeignKeyResolver compositeForeignKeyResolver; 085 086 private LegacyAdapterContext(Builder builder) { 087 this.sourceKind = required(builder.sourceKind, "sourceKind"); 088 this.logicalLocator = required(builder.logicalLocator, "logicalLocator"); 089 this.sqlDialect = required(builder.sqlDialect, "sqlDialect"); 090 this.contentSha256 = required(builder.contentSha256, "contentSha256"); 091 this.statementKind = required(builder.statementKind, "statementKind"); 092 this.engineVersion = required(builder.engineVersion, "engineVersion"); 093 this.identityResolver = builder.identityResolver == null ? NO_IDENTITY : builder.identityResolver; 094 this.typedTransformResolver = builder.typedTransformResolver == null 095 ? NO_TYPED_TRANSFORM : builder.typedTransformResolver; 096 this.routineBindingResolver = builder.routineBindingResolver == null 097 ? NO_ROUTINE_BINDING : builder.routineBindingResolver; 098 this.compositeForeignKeyResolver = builder.compositeForeignKeyResolver == null 099 ? NO_COMPOSITE_FOREIGN_KEY : builder.compositeForeignKeyResolver; 100 } 101 102 public static Builder builder() { return new Builder(); } 103 104 /** Convenience factory; the SQL is hashed only and is never parsed by the adapter. */ 105 public static Builder forInlineSql(String logicalLocator, String dialect, String sql) { 106 return builder().sourceKind("INLINE_SQL").logicalLocator(logicalLocator).sqlDialect(dialect) 107 .contentSha256(sha256(sql.getBytes(StandardCharsets.UTF_8))); 108 } 109 110 public String sourceKind() { return sourceKind; } 111 public String logicalLocator() { return logicalLocator; } 112 public String sqlDialect() { return sqlDialect; } 113 public String contentSha256() { return contentSha256; } 114 public String statementKind() { return statementKind; } 115 public String engineVersion() { return engineVersion; } 116 public IdentityResolver identityResolver() { return identityResolver; } 117 public TypedTransformResolver typedTransformResolver() { return typedTransformResolver; } 118 public RoutineBindingResolver routineBindingResolver() { return routineBindingResolver; } 119 public CompositeForeignKeyResolver compositeForeignKeyResolver() { return compositeForeignKeyResolver; } 120 121 public static final class TypedTransform { 122 private final String operationFamily; 123 private final String operationCode; 124 private final List<TypedOperand> operands; 125 126 public TypedTransform(String operationFamily, String operationCode, List<TypedOperand> operands) { 127 this.operationFamily = required(operationFamily, "operationFamily"); 128 this.operationCode = required(operationCode, "operationCode"); 129 if (operands == null || operands.isEmpty()) { 130 throw new IllegalArgumentException("operands must not be empty"); 131 } 132 this.operands = Collections.unmodifiableList(new ArrayList<TypedOperand>(operands)); 133 } 134 135 public String operationFamily() { return operationFamily; } 136 public String operationCode() { return operationCode; } 137 public List<TypedOperand> operands() { return operands; } 138 } 139 140 public static final class TypedOperand { 141 /** Legacy POJO record ID used only for edge correlation, never a database identifier. */ 142 private final String sourceId; 143 private final String roleCode; 144 145 public TypedOperand(String sourceId, String roleCode) { 146 this.sourceId = required(sourceId, "sourceId"); 147 this.roleCode = required(roleCode, "roleCode"); 148 } 149 150 public String sourceId() { return sourceId; } 151 public String roleCode() { return roleCode; } 152 } 153 154 public static final class RoutineBinding { 155 private final ResolvedIdentity routineIdentity; 156 private final List<RoutineParameterBinding> parameters; 157 private final List<RoutineValueFlow> flows; 158 159 public RoutineBinding(ResolvedIdentity routineIdentity, List<RoutineParameterBinding> parameters, 160 List<RoutineValueFlow> flows) { 161 if (routineIdentity == null) throw new IllegalArgumentException("routineIdentity must not be null"); 162 this.routineIdentity = routineIdentity; 163 this.parameters = immutable(parameters); 164 this.flows = immutable(flows); 165 } 166 167 public ResolvedIdentity routineIdentity() { return routineIdentity; } 168 public List<RoutineParameterBinding> parameters() { return parameters; } 169 public List<RoutineValueFlow> flows() { return flows; } 170 } 171 172 public static final class RoutineParameterBinding { 173 private final String bindingKey; 174 private final String legacyArgumentId; 175 private final String boundLegacyEndpointId; 176 private final String displayName; 177 private final String role; 178 private final int ordinal; 179 private final String declaredType; 180 private final ResolvedIdentity identity; 181 182 public RoutineParameterBinding(String bindingKey, String legacyArgumentId, String boundLegacyEndpointId, 183 String displayName, String role, int ordinal, String declaredType, ResolvedIdentity identity) { 184 this.bindingKey = required(bindingKey, "bindingKey"); 185 this.legacyArgumentId = optional(legacyArgumentId); 186 this.boundLegacyEndpointId = optional(boundLegacyEndpointId); 187 this.displayName = required(displayName, "displayName"); 188 this.role = required(role, "role"); 189 if (ordinal < 1) throw new IllegalArgumentException("ordinal must be positive"); 190 this.ordinal = ordinal; 191 this.declaredType = optional(declaredType); 192 if (identity == null) throw new IllegalArgumentException("identity must not be null"); 193 this.identity = identity; 194 } 195 196 public String bindingKey() { return bindingKey; } 197 public String legacyArgumentId() { return legacyArgumentId; } 198 public String boundLegacyEndpointId() { return boundLegacyEndpointId; } 199 public String displayName() { return displayName; } 200 public String role() { return role; } 201 public int ordinal() { return ordinal; } 202 public String declaredType() { return declaredType; } 203 public ResolvedIdentity identity() { return identity; } 204 } 205 206 public static final class RoutineValueRef { 207 private final String parameterBindingKey; 208 private final String legacyEndpointId; 209 210 private RoutineValueRef(String parameterBindingKey, String legacyEndpointId) { 211 this.parameterBindingKey = optional(parameterBindingKey); 212 this.legacyEndpointId = optional(legacyEndpointId); 213 if ((this.parameterBindingKey == null) == (this.legacyEndpointId == null)) { 214 throw new IllegalArgumentException("exactly one routine value reference kind is required"); 215 } 216 } 217 218 public static RoutineValueRef parameter(String bindingKey) { 219 return new RoutineValueRef(required(bindingKey, "bindingKey"), null); 220 } 221 222 public static RoutineValueRef legacyEndpoint(String legacyEndpointId) { 223 return new RoutineValueRef(null, required(legacyEndpointId, "legacyEndpointId")); 224 } 225 226 public String parameterBindingKey() { return parameterBindingKey; } 227 public String legacyEndpointId() { return legacyEndpointId; } 228 } 229 230 public static final class RoutineValueFlow { 231 private final RoutineValueRef source; 232 private final RoutineValueRef target; 233 private final String role; 234 private final String operationKind; 235 private final String operationInputKind; 236 237 public RoutineValueFlow(RoutineValueRef source, RoutineValueRef target, String role, 238 String operationKind, String operationInputKind) { 239 if (source == null || target == null) throw new IllegalArgumentException("routine flow endpoints are required"); 240 this.source = source; 241 this.target = target; 242 this.role = required(role, "role"); 243 this.operationKind = required(operationKind, "operationKind"); 244 this.operationInputKind = required(operationInputKind, "operationInputKind"); 245 } 246 247 public RoutineValueRef source() { return source; } 248 public RoutineValueRef target() { return target; } 249 public String role() { return role; } 250 public String operationKind() { return operationKind; } 251 public String operationInputKind() { return operationInputKind; } 252 } 253 254 public static final class ForeignKeyBinding { 255 private final List<String> evidenceRelationshipIds; 256 private final List<ForeignKeyPair> keyPairs; 257 258 public ForeignKeyBinding(List<String> evidenceRelationshipIds, List<ForeignKeyPair> keyPairs) { 259 this.evidenceRelationshipIds = immutableRequired(evidenceRelationshipIds, "evidenceRelationshipIds"); 260 this.keyPairs = immutableRequired(keyPairs, "keyPairs"); 261 } 262 263 public List<String> evidenceRelationshipIds() { return evidenceRelationshipIds; } 264 public List<ForeignKeyPair> keyPairs() { return keyPairs; } 265 } 266 267 public static final class ForeignKeyPair { 268 private final String foreignKeyFieldLegacyId; 269 private final String referencedFieldLegacyId; 270 271 public ForeignKeyPair(String foreignKeyFieldLegacyId, String referencedFieldLegacyId) { 272 this.foreignKeyFieldLegacyId = required(foreignKeyFieldLegacyId, "foreignKeyFieldLegacyId"); 273 this.referencedFieldLegacyId = required(referencedFieldLegacyId, "referencedFieldLegacyId"); 274 } 275 276 public String foreignKeyFieldLegacyId() { return foreignKeyFieldLegacyId; } 277 public String referencedFieldLegacyId() { return referencedFieldLegacyId; } 278 } 279 280 public static final class ResolvedIdentity { 281 private final SlpcObject identity; 282 private final String resolutionStatus; 283 private final String publicationState; 284 285 public ResolvedIdentity(SlpcObject identity, String resolutionStatus, String publicationState) { 286 if (identity == null) throw new IllegalArgumentException("identity must not be null"); 287 this.identity = identity; 288 this.resolutionStatus = required(resolutionStatus, "resolutionStatus"); 289 this.publicationState = required(publicationState, "publicationState"); 290 } 291 292 public SlpcObject identity() { return identity; } 293 public String resolutionStatus() { return resolutionStatus; } 294 public String publicationState() { return publicationState; } 295 } 296 297 public static final class Builder { 298 private String sourceKind = "INLINE_SQL"; 299 private String logicalLocator; 300 private String sqlDialect; 301 private String contentSha256; 302 private String statementKind = "UNKNOWN"; 303 private String engineVersion = TBaseType.versionid; 304 private IdentityResolver identityResolver; 305 private TypedTransformResolver typedTransformResolver; 306 private RoutineBindingResolver routineBindingResolver; 307 private CompositeForeignKeyResolver compositeForeignKeyResolver; 308 309 public Builder sourceKind(String value) { this.sourceKind = value; return this; } 310 public Builder logicalLocator(String value) { this.logicalLocator = value; return this; } 311 public Builder sqlDialect(String value) { this.sqlDialect = value; return this; } 312 public Builder contentSha256(String value) { this.contentSha256 = value; return this; } 313 public Builder statementKind(String value) { this.statementKind = value; return this; } 314 public Builder engineVersion(String value) { this.engineVersion = value; return this; } 315 public Builder identityResolver(IdentityResolver value) { this.identityResolver = value; return this; } 316 public Builder typedTransformResolver(TypedTransformResolver value) { this.typedTransformResolver = value; return this; } 317 public Builder routineBindingResolver(RoutineBindingResolver value) { this.routineBindingResolver = value; return this; } 318 public Builder compositeForeignKeyResolver(CompositeForeignKeyResolver value) { this.compositeForeignKeyResolver = value; return this; } 319 /** 320 * Connects evidence captured by the same DataFlowAnalyzer run to all 321 * AST/catalog-authoritative adapter resolvers. Explicit resolver calls 322 * made after this method may override an individual strategy. 323 */ 324 public Builder authoritativeEvidence(AuthoritativeLineageEvidence value) { 325 LegacyAuthoritativeEvidenceBridge bridge = new LegacyAuthoritativeEvidenceBridge(value); 326 this.typedTransformResolver = bridge; 327 this.routineBindingResolver = bridge; 328 this.compositeForeignKeyResolver = bridge; 329 return this; 330 } 331 public LegacyAdapterContext build() { return new LegacyAdapterContext(this); } 332 } 333 334 private static <T> List<T> immutable(List<T> values) { 335 return values == null ? Collections.<T>emptyList() 336 : Collections.unmodifiableList(new ArrayList<T>(values)); 337 } 338 339 private static <T> List<T> immutableRequired(List<T> values, String field) { 340 if (values == null || values.isEmpty()) throw new IllegalArgumentException(field + " must not be empty"); 341 return Collections.unmodifiableList(new ArrayList<T>(values)); 342 } 343 344 private static String optional(String value) { 345 return value == null || value.isEmpty() ? null : value; 346 } 347 348 private static String required(String value, String field) { 349 if (value == null || value.isEmpty()) throw new IllegalArgumentException(field + " must not be empty"); 350 return value; 351 } 352 353 private static String sha256(byte[] value) { 354 try { 355 byte[] digest = MessageDigest.getInstance("SHA-256").digest(value); 356 StringBuilder result = new StringBuilder(); 357 for (byte item : digest) result.append(String.format("%02x", item & 255)); 358 return result.toString(); 359 } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); } 360 } 361}