001package gudusoft.gsqlparser.dlineage.dynamicsql; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TStatementList; 005import gudusoft.gsqlparser.dlineage.dynamicsql.RoutineSummaryEdge.Endpoint; 006import gudusoft.gsqlparser.dlineage.dynamicsql.RoutineSummaryEdge.EndpointKind; 007import gudusoft.gsqlparser.nodes.TObjectName; 008 009import java.util.ArrayDeque; 010import java.util.ArrayList; 011import java.util.Collections; 012import java.util.Comparator; 013import java.util.Deque; 014import java.util.HashMap; 015import java.util.HashSet; 016import java.util.LinkedHashMap; 017import java.util.LinkedHashSet; 018import java.util.List; 019import java.util.Map; 020import java.util.Set; 021import java.util.TreeSet; 022 023/** 024 * B3 (design {@code routine-summary-scc-design.md} §2.3/§2.6): SHADOW-mode 025 * summary composition into an ISOLATED observation model. Nothing here touches the 026 * LEGACY analysis — the applier runs its own per-definition extractions, 027 * binds call sites through the {@link CatalogRoutineCallBinder}, and emits 028 * the composed cross-routine edges as a standalone edge set tagged 029 * {@code origin=SUMMARY_COMPOSED}. The {@link ShadowLeafEdgeComparator} 030 * classifies that set against the LEGACY run's leaf projection. 031 * 032 * <p>Composition rules (the G1 capability): 033 * <ul> 034 * <li><b>IN</b>: callee edge {@code formal_p → B} composes with the caller's 035 * boundary sources feeding the actual bound to {@code p} — the fan-in of 036 * the actual expression's leaves, resolved through the caller's OWN 037 * closure (locals are quotiented out of summaries; the per-call-site 038 * reach on {@link CallSiteRef} is exactly what composition needs).</li> 039 * <li><b>OUT</b>: callee edge {@code X → formal_p(OUT)} composes onto the 040 * caller boundary endpoints reachable FROM the bound writable lvalue. 041 * When {@code X} is itself an IN formal of the callee, it chains through 042 * the caller sources of ITS actual (IN→OUT pass-through).</li> 043 * <li><b>RESULT SET</b>: callee edge {@code X → resultset column} maps 044 * positionally onto INSERT…EXEC target columns (§2.3 rule 3), using the 045 * callee summary's recorded result-set shape.</li> 046 * <li>Relation flavors compose as the closure does: a chain is {@code fdd} 047 * only when every leg is; any {@code fdr} leg makes it {@code fdr}.</li> 048 * <li>AMBIGUOUS / UNRESOLVED / duplicate-identity callees bind nothing — 049 * a diagnostic records each skip (never guessed).</li> 050 * </ul> 051 */ 052public final class ShadowSummaryApplier { 053 054 /** Stable reason attached to every composed observation until call 055 * validity, type compatibility, actual mutability, and execution context 056 * are exhaustively proven. */ 057 public static final String APPLICATION_DEFERRED_CALL_VALIDITY_NOT_PROVEN = 058 "APPLICATION_DEFERRED_CALL_VALIDITY_NOT_PROVEN"; 059 060 /** Publication gate attached to every composed edge. No dataflow model 061 * is mutated by this class. Current output is observation-only; 062 * REPLACEMENT_ELIGIBLE is reserved and unreachable. */ 063 public enum ApplicationGate { 064 /** Reserved for a future exhaustively-preflighted projection path. 065 * Current public results never emit this verdict. */ 066 REPLACEMENT_ELIGIBLE, 067 /** Candidate edge for evaluation; not authorized for merging. */ 068 OBSERVATION_ONLY 069 } 070 071 public enum EdgeOrigin { 072 SUMMARY_COMPOSED 073 } 074 075 /** One composed cross-routine edge of the isolated SHADOW side model. */ 076 public static final class ComposedEdge { 077 private final Endpoint source; 078 private final Endpoint target; 079 private final String relationType; 080 private final RoutineIdentity callerIdentity; 081 private final RoutineIdentity calleeIdentity; 082 private final int callSiteOrdinal; 083 private final Set<String> observedFunctions; 084 private final RoutineSummary.Completeness calleeCompleteness; 085 private final ApplicationGate applicationGate; 086 private final String applicationDeferralReason; 087 088 public ComposedEdge(Endpoint source, Endpoint target, String relationType, 089 RoutineIdentity callerIdentity, RoutineIdentity calleeIdentity, 090 int callSiteOrdinal) { 091 this(source, target, relationType, callerIdentity, calleeIdentity, 092 callSiteOrdinal, null, RoutineSummary.Completeness.PARTIAL); 093 } 094 095 ComposedEdge(Endpoint source, Endpoint target, String relationType, 096 RoutineIdentity callerIdentity, RoutineIdentity calleeIdentity, 097 int callSiteOrdinal, Set<String> observedFunctions, 098 RoutineSummary.Completeness calleeCompleteness) { 099 this.source = source; 100 this.target = target; 101 this.relationType = relationType; 102 this.callerIdentity = callerIdentity; 103 this.calleeIdentity = calleeIdentity; 104 this.callSiteOrdinal = callSiteOrdinal; 105 this.observedFunctions = observedFunctions == null 106 ? Collections.<String>emptySet() 107 : Collections.unmodifiableSet( 108 new TreeSet<String>(observedFunctions)); 109 this.calleeCompleteness = calleeCompleteness == null 110 ? RoutineSummary.Completeness.PARTIAL : calleeCompleteness; 111 this.applicationGate = ApplicationGate.OBSERVATION_ONLY; 112 this.applicationDeferralReason = 113 APPLICATION_DEFERRED_CALL_VALIDITY_NOT_PROVEN; 114 } 115 116 public Endpoint getSource() { return source; } 117 public Endpoint getTarget() { return target; } 118 public String getRelationType() { return relationType; } 119 /** Composing caller routine (occurrence identity component). */ 120 public RoutineIdentity getCallerIdentity() { return callerIdentity; } 121 /** Composed callee routine (occurrence identity component). */ 122 public RoutineIdentity getCalleeIdentity() { return calleeIdentity; } 123 /** Display rendering of the caller identity. */ 124 public String getCallerName() { return String.valueOf(callerIdentity); } 125 /** Display rendering of the callee identity. */ 126 public String getCalleeName() { return String.valueOf(calleeIdentity); } 127 /** Position of the composing call site in the caller's call-site 128 * list (occurrence identity component). */ 129 public int getCallSiteOrdinal() { return callSiteOrdinal; } 130 /** Unordered advisory names observed on contributing paths. */ 131 public Set<String> getObservedFunctions() { return observedFunctions; } 132 /** @deprecated use {@link #getObservedFunctions()}. */ 133 @Deprecated 134 public Set<String> getTransforms() { return observedFunctions; } 135 public RoutineSummary.Completeness getCalleeCompleteness() { 136 return calleeCompleteness; 137 } 138 public ApplicationGate getApplicationGate() { return applicationGate; } 139 /** Stable reason this observation is not an applicable addition. */ 140 public String getApplicationDeferralReason() { 141 return applicationDeferralReason; 142 } 143 public EdgeOrigin getOrigin() { return EdgeOrigin.SUMMARY_COMPOSED; } 144 145 ComposedEdge withMetadata(Set<String> joined, 146 RoutineSummary.Completeness joinedCompleteness) { 147 return new ComposedEdge(source, target, relationType, 148 callerIdentity, calleeIdentity, callSiteOrdinal, joined, 149 joinedCompleteness); 150 } 151 152 /** OCCURRENCE identity (codex-B3-r2 finding 8 ruling, tightened per 153 * r3 finding 3): FULL RoutineIdentity equality for caller and 154 * callee — never a rendering, which omits signature fields and 155 * collapsed distinct same-name/same-arity identities. */ 156 @Override 157 public boolean equals(Object o) { 158 if (this == o) { 159 return true; 160 } 161 if (!(o instanceof ComposedEdge)) { 162 return false; 163 } 164 ComposedEdge e = (ComposedEdge) o; 165 // non-identifier-compare: relationType is a fixed enum-like token; 166 // identities compare via RoutineIdentity.equals (canonical fields) 167 return source.equals(e.source) && target.equals(e.target) 168 && relationType.equals(e.relationType) 169 && callerIdentity.equals(e.callerIdentity) 170 && calleeIdentity.equals(e.calleeIdentity) 171 && callSiteOrdinal == e.callSiteOrdinal; 172 } 173 174 @Override 175 public int hashCode() { 176 int h = (source.hashCode() * 31 + target.hashCode()) * 31 177 + relationType.hashCode(); 178 h = h * 31 + callerIdentity.hashCode(); 179 h = h * 31 + calleeIdentity.hashCode(); 180 h = h * 31 + callSiteOrdinal; 181 return h; 182 } 183 184 @Override 185 public String toString() { 186 return source + " -" + relationType + "-> " + target 187 + " [SUMMARY_COMPOSED " + callerIdentity + " -> " 188 // Keep the pre-B6 debug rendering stable. Consumers that 189 // need the trust verdict use getApplicationGate(). 190 + calleeIdentity + "]"; 191 } 192 } 193 194 /** The isolated SHADOW observation output. */ 195 public static final class ShadowResult { 196 private final Set<ComposedEdge> additions; 197 private final List<String> diagnostics; 198 private final Map<RoutineIdentity, RoutineSummary> summaries; 199 private final Map<RoutineIdentity, Integer> fixedPointIterations; 200 201 ShadowResult(Set<ComposedEdge> additions, List<String> diagnostics, 202 Map<RoutineIdentity, RoutineSummary> summaries) { 203 this(additions, diagnostics, summaries, 204 Collections.<RoutineIdentity, Integer>emptyMap()); 205 } 206 207 ShadowResult(Set<ComposedEdge> additions, List<String> diagnostics, 208 Map<RoutineIdentity, RoutineSummary> summaries, 209 Map<RoutineIdentity, Integer> fixedPointIterations) { 210 this.additions = Collections.unmodifiableSet( 211 new LinkedHashSet<ComposedEdge>(additions)); 212 this.diagnostics = Collections.unmodifiableList( 213 new ArrayList<String>(diagnostics)); 214 this.summaries = Collections.unmodifiableMap( 215 new LinkedHashMap<RoutineIdentity, RoutineSummary>(summaries)); 216 this.fixedPointIterations = Collections.unmodifiableMap( 217 new LinkedHashMap<RoutineIdentity, Integer>( 218 fixedPointIterations)); 219 } 220 221 public Set<ComposedEdge> getAdditions() { return additions; } 222 public List<String> getDiagnostics() { return diagnostics; } 223 public Map<RoutineIdentity, RoutineSummary> getSummaries() { return summaries; } 224 /** Number of synchronous fixed-point rounds run for each routine's 225 * SCC; zero means no summary evaluation (LEGACY). */ 226 public Map<RoutineIdentity, Integer> getFixedPointIterations() { 227 return fixedPointIterations; 228 } 229 } 230 231 private final EDbVendor vendor; 232 private final ProceduralLineageOptions options; 233 234 public ShadowSummaryApplier(EDbVendor vendor) { 235 this(vendor, ProceduralLineageOptions.shadow()); 236 } 237 238 public ShadowSummaryApplier(EDbVendor vendor, 239 ProceduralLineageOptions options) { 240 this.vendor = vendor; 241 this.options = options == null 242 ? ProceduralLineageOptions.legacy() : options; 243 } 244 245 /** 246 * Run the full SHADOW pipeline over a multi-file unit: catalog, base 247 * summaries, call-site binding (identity-first: the resolved 248 * {@link RoutineIdentity} is the summary lookup key), composition. 249 */ 250 public ShadowResult apply(List<String> unitTexts) { 251 if (options.getMode() == ProceduralLineageOptions.Mode.LEGACY) { 252 return new ShadowResult(Collections.<ComposedEdge>emptySet(), 253 Collections.<String>emptyList(), 254 Collections.<RoutineIdentity, RoutineSummary>emptyMap(), 255 Collections.<RoutineIdentity, Integer>emptyMap()); 256 } 257 List<String> diagnostics = new ArrayList<String>(); 258 diagnostics.add(APPLICATION_DEFERRED_CALL_VALIDITY_NOT_PROVEN 259 + ": composed procedure lineage is observation-only"); 260 RoutineCatalog catalog = new RoutineCatalog(vendor, unitTexts, 261 new HashMap<String, TStatementList>()); 262 CatalogRoutineCallBinder binder = new CatalogRoutineCallBinder(vendor, catalog); 263 RoutineSummaryExtractor extractor = new RoutineSummaryExtractor(vendor); 264 265 // Identity-keyed summary index. Duplicate definitions of one identity 266 // stay AMBIGUOUS: they bind nothing and are removed (codex-C5). 267 Map<RoutineIdentity, RoutineSummary> summaries = 268 new LinkedHashMap<RoutineIdentity, RoutineSummary>(); 269 Set<RoutineIdentity> duplicates = new LinkedHashSet<RoutineIdentity>(); 270 int unitOrdinal = -1; 271 for (String unitText : unitTexts) { 272 unitOrdinal++; 273 for (RoutineSummary summary : extractor.extractAll(unitText)) { 274 RoutineIdentity id = summary.getIdentity(); 275 if (id == null) { 276 String code = summary.getCompletenessReasons().contains( 277 "PARSE_FAILED") 278 ? "OPAQUE_UNIT_PARSE_FAILED" 279 : "OPAQUE_UNIT_IDENTITY_UNAVAILABLE"; 280 diagnostics.add(code + ": unit=" + unitOrdinal 281 + " reasons=" + summary.getCompletenessReasons()); 282 continue; 283 } 284 if (summaries.containsKey(id)) { 285 duplicates.add(id); 286 } else { 287 summaries.put(id, summary); 288 } 289 } 290 } 291 for (RoutineIdentity dup : duplicates) { 292 summaries.remove(dup); 293 diagnostics.add("DUPLICATE_DEFINITION: " + dup); 294 } 295 296 // Caller effective database (mssql USE context) per definition. 297 Map<RoutineIdentity, String> callerDb = new HashMap<RoutineIdentity, String>(); 298 for (String unitText : unitTexts) { 299 RoutineCatalog.UnitIndex index = catalog.indexOf(unitText); 300 if (index == null) { 301 continue; 302 } 303 for (RoutineCatalog.Definition def : index.definitions) { 304 if (def.identity != null) { 305 callerDb.put(def.identity, def.db); 306 } 307 } 308 } 309 310 // Freeze every call target ONCE before fixed-point evaluation. No 311 // summary-derived type/effect information is ever fed back into the 312 // binder (§2.5 frozen-graph invariant). 313 Map<RoutineIdentity, List<SiteBinding>> frozenCalls = 314 new LinkedHashMap<RoutineIdentity, List<SiteBinding>>(); 315 Map<RoutineIdentity, List<String>> localReasons = 316 new LinkedHashMap<RoutineIdentity, List<String>>(); 317 for (RoutineIdentity callerId : orderedIdentities(summaries.keySet())) { 318 RoutineSummary caller = summaries.get(callerId); 319 String callerName = callerId.toString(); 320 Set<String> reasons = new TreeSet<String>(); 321 // The extractor's call inventory and every callee-edge projection 322 // are not yet exhaustively preflighted. Until that proof exists, 323 // no evaluated public summary can authorize replacement. 324 reasons.add("CALL_INVENTORY_NOT_PROVEN"); 325 for (String reason : caller.getCompletenessReasons()) { 326 // Base extraction marks every observed call unresolved. The 327 // frozen binder verdict below replaces that provisional gate. 328 if (!"CALLS_UNRESOLVED".equals(reason)) { // non-identifier-compare: completeness reason token 329 reasons.add(reason); 330 } 331 } 332 List<SiteBinding> resolved = new ArrayList<SiteBinding>(); 333 int callSiteOrdinal = -1; 334 for (CallSiteRef site : caller.getCallSites()) { 335 callSiteOrdinal++; 336 CatalogRoutineCallBinder.BoundCall bound = 337 binder.bind(site, callerDb.get(caller.getIdentity()), 338 callerId); 339 RoutineCallBinder.BindingResult result = bound.getResult(); 340 if (result.getStatus() != RoutineCallBinder.BindingStatus.RESOLVED) { 341 reasons.add("CALLS_UNRESOLVED"); 342 diagnostics.add(result.getStatus() + ": " 343 + site.getRaw().getRawName() + " in " + callerName); 344 addBindingDiagnostics(result, diagnostics); 345 continue; 346 } 347 boolean invalidOutTarget = hasBindingDiagnostic(result, 348 RoutineCallBinder.BindingDiagnosticCode 349 .PROC_CALL_OUT_TARGET_NOT_WRITABLE); 350 boolean outputModeMismatch = hasBindingDiagnostic(result, 351 RoutineCallBinder.BindingDiagnosticCode 352 .PROC_CALL_OUTPUT_MODE_MISMATCH); 353 if (invalidOutTarget || outputModeMismatch) { 354 // Invalid OUT/OUTPUT binding makes the call non-executable. 355 // Quarantine the complete site: even an otherwise 356 // independent callee table effect cannot occur. 357 if (invalidOutTarget) { 358 reasons.add("PROC_CALL_OUT_TARGET_NOT_WRITABLE"); 359 } 360 if (outputModeMismatch) { 361 reasons.add("PROC_CALL_OUTPUT_MODE_MISMATCH"); 362 } 363 reasons.add("CALL_BINDING_PARTIAL"); 364 addBindingDiagnostics(result, diagnostics); 365 continue; 366 } 367 RoutineSummary callee = summaries.get(result.getResolved()); 368 if (callee == null) { 369 reasons.add("CALLEE_SUMMARY_UNAVAILABLE"); 370 diagnostics.add("CALLEE_SUMMARY_UNAVAILABLE: " 371 + site.getRaw().getRawName() + " in " + callerName); 372 continue; 373 } 374 if (callee.getCompleteness() == RoutineSummary.Completeness.OPAQUE) { 375 reasons.add("CALLEE_OPAQUE"); 376 diagnostics.add("CALLEE_OPAQUE: " 377 + site.getRaw().getRawName() + " in " + callerName); 378 continue; 379 } 380 boolean resultProjectionProven = true; 381 if (result.getResultSets() != null) { 382 int targetCount = result.getResultSets() 383 .getPositionalTargets().size(); 384 List<String> arityMismatches = resultSetArityMismatches( 385 callee, targetCount); 386 List<String> duplicateAliases = duplicateAliasShapes(callee); 387 List<String> unresolvedProjections = 388 unresolvedResultSetProjections(callee); 389 List<String> duplicateTargets = 390 duplicateInsertTargets(result.getResultSets() 391 .getPositionalTargets()); 392 resultProjectionProven = arityMismatches.isEmpty() 393 && duplicateAliases.isEmpty() 394 && unresolvedProjections.isEmpty() 395 && duplicateTargets.isEmpty() 396 && !callee.getResultSetShapes().isEmpty(); 397 for (String mismatch : arityMismatches) { 398 reasons.add("RESULT_SET_MAPPING_ARITY_MISMATCH"); 399 diagnostics.add("RESULT_SET_MAPPING_ARITY_MISMATCH: " 400 + site.getRaw().getRawName() + " in " 401 + callerName + " " + mismatch); 402 } 403 for (String shapeName : duplicateAliases) { 404 reasons.add("RESULT_SET_MAPPING_AMBIGUOUS"); 405 diagnostics.add("RESULT_SET_MAPPING_AMBIGUOUS: " 406 + site.getRaw().getRawName() + " in " 407 + callerName + " shape=" + shapeName); 408 } 409 for (String projection : unresolvedProjections) { 410 reasons.add("RESULT_SET_MAPPING_UNRESOLVED"); 411 diagnostics.add("RESULT_SET_MAPPING_UNRESOLVED: " 412 + site.getRaw().getRawName() + " in " 413 + callerName + " projection=" + projection); 414 } 415 for (String duplicate : duplicateTargets) { 416 reasons.add("RESULT_SET_MAPPING_TARGET_DUPLICATE"); 417 diagnostics.add( 418 "RESULT_SET_MAPPING_TARGET_DUPLICATE: " 419 + site.getRaw().getRawName() + " in " 420 + callerName + " " + duplicate); 421 } 422 } 423 if (result.getCompleteness() 424 == RoutineCallBinder.BindingCompleteness.PARTIAL) { 425 reasons.add("CALL_BINDING_PARTIAL"); 426 } 427 addBindingDiagnostics(result, diagnostics); 428 resolved.add(new SiteBinding(site, bound, callee, 429 callSiteOrdinal, resultProjectionProven)); 430 } 431 if (caller.getCalls().size() > caller.getCallSites().size()) { 432 reasons.add("CALLS_UNRESOLVED"); 433 } 434 frozenCalls.put(callerId, resolved); 435 localReasons.put(callerId, new ArrayList<String>(reasons)); 436 } 437 438 Evaluation evaluation = evaluateFixedPoint(summaries, frozenCalls, 439 localReasons, diagnostics); 440 441 // Project each frozen call against the evaluated callee. This remains 442 // an isolated observation side model; even ENHANCED never mutates the 443 // legacy dataflow object or exposes applicable additions. 444 Map<ComposedEdge, ComposedEdge> additions = 445 new LinkedHashMap<ComposedEdge, ComposedEdge>(); 446 for (RoutineIdentity callerId 447 : orderedIdentities(evaluation.summaries.keySet())) { 448 List<SiteBinding> evaluatedCalls = callsWithEvaluatedCallees( 449 frozenCalls.get(callerId), evaluation.summaries); 450 for (SiteBinding call : evaluatedCalls) { 451 composeCallSite(call.site, call.bound, call.callee, callerId, 452 call.ordinal, call.resultProjectionProven, additions); 453 } 454 composeBridges(callerId, evaluatedCalls, additions); 455 } 456 Set<ComposedEdge> orderedAdditionSet = orderedAdditions(additions); 457 Collections.sort(diagnostics); 458 return new ShadowResult(orderedAdditionSet, diagnostics, 459 evaluation.summaries, 460 evaluation.iterations); 461 } 462 463 private List<String> duplicateAliasShapes(RoutineSummary callee) { 464 List<String> ambiguous = new ArrayList<String>(); 465 for (Map.Entry<String, List<String>> entry 466 : callee.getResultSetShapes().entrySet()) { 467 List<String> shape = entry.getValue(); 468 if (hasAmbiguousShapeAliases(shape)) { 469 ambiguous.add(entry.getKey()); 470 } 471 } 472 Collections.sort(ambiguous); 473 return ambiguous; 474 } 475 476 private static List<String> resultSetArityMismatches( 477 RoutineSummary callee, int targetCount) { 478 List<String> mismatches = new ArrayList<String>(); 479 for (Map.Entry<String, List<String>> entry 480 : callee.getResultSetShapes().entrySet()) { 481 int outputCount = entry.getValue().size(); 482 if (outputCount != targetCount) { 483 mismatches.add("shape=" + entry.getKey() 484 + " outputs=" + outputCount 485 + " targets=" + targetCount); 486 } 487 } 488 Collections.sort(mismatches); 489 return mismatches; 490 } 491 492 private List<String> unresolvedResultSetProjections( 493 RoutineSummary callee) { 494 Set<String> unresolved = new TreeSet<String>(); 495 if (callee.getResultSetShapes().isEmpty()) { 496 unresolved.add("NO_RESULT_SHAPE"); 497 } 498 for (RoutineSummaryEdge edge : callee.getEdges()) { 499 Endpoint target = edge.getTarget(); 500 if (target.getKind() != EndpointKind.RESULT_SET_COLUMN) { 501 continue; 502 } 503 List<String> shape = callee.getResultSetShapes().get( 504 target.getParent()); 505 // Duplicate shapes have their more specific diagnostic above. 506 if (shape != null && hasAmbiguousShapeAliases(shape)) { 507 continue; 508 } 509 if (shape == null 510 || uniqueShapeOrdinal(shape, target.getColumn()) < 0) { 511 unresolved.add(target.getParent() + "." + target.getColumn()); 512 } 513 } 514 return new ArrayList<String>(unresolved); 515 } 516 517 /** Duplicate explicit INSERT targets make INSERT...EXEC invalid in SQL 518 * Server. Compare with the vendor identifier facade; spelling/case must 519 * not decide whether a target list is accepted. */ 520 private List<String> duplicateInsertTargets(List<TObjectName> targets) { 521 List<String> duplicates = new ArrayList<String>(); 522 if (targets == null) { 523 return duplicates; 524 } 525 for (int i = 0; i < targets.size(); i++) { 526 for (int j = i + 1; j < targets.size(); j++) { 527 String left = targets.get(i).toString(); 528 String right = targets.get(j).toString(); 529 if (sameShapeColumn(left, right)) { 530 duplicates.add("target=" + left + " positions=" 531 + (i + 1) + "," + (j + 1)); 532 } 533 } 534 } 535 Collections.sort(duplicates); 536 return duplicates; 537 } 538 539 private static void addBindingDiagnostics( 540 RoutineCallBinder.BindingResult result, List<String> diagnostics) { 541 for (RoutineCallBinder.BindingDiagnostic d : result.getDiagnostics()) { 542 diagnostics.add(d.getCode() + ": " + d.getDetail()); 543 } 544 } 545 546 private static boolean hasBindingDiagnostic( 547 RoutineCallBinder.BindingResult result, 548 RoutineCallBinder.BindingDiagnosticCode code) { 549 for (RoutineCallBinder.BindingDiagnostic diagnostic 550 : result.getDiagnostics()) { 551 if (diagnostic.getCode() == code) { 552 return true; 553 } 554 } 555 return false; 556 } 557 558 /** One RESOLVED call site with its binding and callee summary. */ 559 private static final class SiteBinding { 560 final CallSiteRef site; 561 final CatalogRoutineCallBinder.BoundCall bound; 562 final RoutineSummary callee; 563 final int ordinal; 564 /** All result shapes at this call site passed cardinality, alias, and 565 * endpoint-to-shape preflight. False suppresses every result-set 566 * projection while preserving OUT and table-side-effect edges. */ 567 final boolean resultProjectionProven; 568 569 SiteBinding(CallSiteRef site, CatalogRoutineCallBinder.BoundCall bound, 570 RoutineSummary callee, int ordinal, 571 boolean resultProjectionProven) { 572 this.site = site; 573 this.bound = bound; 574 this.callee = callee; 575 this.ordinal = ordinal; 576 this.resultProjectionProven = resultProjectionProven; 577 } 578 579 SiteBinding withCallee(RoutineSummary evaluated) { 580 return new SiteBinding(site, bound, evaluated, ordinal, 581 resultProjectionProven); 582 } 583 } 584 585 private static final class Evaluation { 586 final Map<RoutineIdentity, RoutineSummary> summaries; 587 final Map<RoutineIdentity, Integer> iterations; 588 589 Evaluation(Map<RoutineIdentity, RoutineSummary> summaries, 590 Map<RoutineIdentity, Integer> iterations) { 591 this.summaries = summaries; 592 this.iterations = iterations; 593 } 594 } 595 596 /** 597 * B5: evaluate the frozen routine-call graph one SCC at a time, callees 598 * before callers. Within a recursive component updates are synchronous 599 * monotone joins over a finite edge/advisory-function lattice. A cap preserves 600 * the accumulated under-approximation and changes only completeness. 601 */ 602 private Evaluation evaluateFixedPoint( 603 Map<RoutineIdentity, RoutineSummary> baseSummaries, 604 Map<RoutineIdentity, List<SiteBinding>> frozenCalls, 605 Map<RoutineIdentity, List<String>> localReasons, 606 List<String> diagnostics) { 607 Map<RoutineIdentity, RoutineSummary> evaluated = 608 new LinkedHashMap<RoutineIdentity, RoutineSummary>(); 609 Map<RoutineIdentity, Integer> iterations = 610 new LinkedHashMap<RoutineIdentity, Integer>(); 611 List<List<RoutineIdentity>> components = calleeFirstComponents( 612 baseSummaries.keySet(), frozenCalls); 613 for (List<RoutineIdentity> component : components) { 614 Set<RoutineIdentity> members = 615 new HashSet<RoutineIdentity>(component); 616 Map<RoutineIdentity, Set<String>> reasons = 617 componentReasons(component, members, frozenCalls, 618 localReasons, evaluated); 619 Map<RoutineIdentity, RoutineSummary> current = 620 new LinkedHashMap<RoutineIdentity, RoutineSummary>(); 621 for (RoutineIdentity id : component) { 622 RoutineSummary base = baseSummaries.get(id); 623 current.put(id, base.evaluated(base.getEdges(), 624 completenessOf(base, reasons.get(id)), 625 new ArrayList<String>(reasons.get(id)))); 626 } 627 628 int cap = options.iterationCapForScc(component.size()); 629 int rounds = 0; 630 boolean converged = false; 631 boolean recursive = isRecursiveComponent(component, frozenCalls); 632 while (rounds < cap) { 633 rounds++; 634 Map<RoutineIdentity, RoutineSummary> next = 635 new LinkedHashMap<RoutineIdentity, RoutineSummary>(); 636 for (RoutineIdentity id : component) { 637 RoutineSummary base = baseSummaries.get(id); 638 if (base.getCompleteness() == RoutineSummary.Completeness.OPAQUE) { 639 next.put(id, current.get(id)); 640 continue; 641 } 642 List<SiteBinding> calls = callsForEvaluation( 643 frozenCalls.get(id), current, evaluated); 644 Set<RoutineSummaryEdge> edges = composeSummaryEdges( 645 base, id, calls); 646 next.put(id, base.evaluated(edges, 647 completenessOf(base, reasons.get(id)), 648 new ArrayList<String>(reasons.get(id)))); 649 } 650 if (sameSummaryValues(current, next)) { 651 current = next; 652 converged = true; 653 break; 654 } 655 current = next; 656 // A singleton with no self-edge depends only on already-final 657 // external callees. Its first recomputation is the exact 658 // result; a second no-change probe is unnecessary and an 659 // explicit cap=1 must not fabricate a recursion diagnostic. 660 if (!recursive) { 661 converged = true; 662 break; 663 } 664 } 665 666 if (!converged) { 667 String componentKey = componentKey(component); 668 diagnostics.add("PROC_RECURSION_LIMIT: cap=" + cap 669 + ", component=" + componentKey); 670 for (RoutineIdentity id : component) { 671 RoutineSummary summary = current.get(id); 672 Set<String> cappedReasons = new TreeSet<String>( 673 summary.getCompletenessReasons()); 674 cappedReasons.add("PROC_RECURSION_LIMIT"); 675 current.put(id, summary.evaluated(summary.getEdges(), 676 RoutineSummary.Completeness.PARTIAL, 677 new ArrayList<String>(cappedReasons))); 678 } 679 } 680 for (RoutineIdentity id : component) { 681 evaluated.put(id, current.get(id)); 682 iterations.put(id, Integer.valueOf(rounds)); 683 } 684 } 685 // Re-key in canonical identity order so result iteration and JSON 686 // serialization are independent of file/hash-map order. 687 Map<RoutineIdentity, RoutineSummary> ordered = 688 new LinkedHashMap<RoutineIdentity, RoutineSummary>(); 689 Map<RoutineIdentity, Integer> orderedIterations = 690 new LinkedHashMap<RoutineIdentity, Integer>(); 691 for (RoutineIdentity id : orderedIdentities(evaluated.keySet())) { 692 RoutineSummary summary = evaluated.get(id); 693 ordered.put(id, summary); 694 orderedIterations.put(id, iterations.get(id)); 695 if (summary.getCompleteness() != RoutineSummary.Completeness.COMPLETE) { 696 diagnostics.add("SUMMARY_" + summary.getCompleteness() + ": " 697 + id.signatureKey() + " reasons=" 698 + summary.getCompletenessReasons()); 699 } 700 } 701 return new Evaluation(ordered, orderedIterations); 702 } 703 704 private static boolean isRecursiveComponent(List<RoutineIdentity> component, 705 Map<RoutineIdentity, List<SiteBinding>> frozenCalls) { 706 if (component.size() > 1) { 707 return true; 708 } 709 RoutineIdentity only = component.get(0); 710 List<SiteBinding> calls = frozenCalls.get(only); 711 if (calls != null) { 712 for (SiteBinding call : calls) { 713 if (only.equals(call.bound.getResult().getResolved())) { 714 return true; 715 } 716 } 717 } 718 return false; 719 } 720 721 private RoutineSummary.Completeness completenessOf(RoutineSummary base, 722 Set<String> reasons) { 723 if (base.getCompleteness() == RoutineSummary.Completeness.OPAQUE) { 724 return RoutineSummary.Completeness.OPAQUE; 725 } 726 return reasons == null || reasons.isEmpty() 727 ? RoutineSummary.Completeness.COMPLETE 728 : RoutineSummary.Completeness.PARTIAL; 729 } 730 731 /** Local reasons plus monotone partialness propagation through the frozen 732 * component. Only a bounded callee marker propagates, never recursively 733 * nested reason text. */ 734 private Map<RoutineIdentity, Set<String>> componentReasons( 735 List<RoutineIdentity> component, Set<RoutineIdentity> members, 736 Map<RoutineIdentity, List<SiteBinding>> frozenCalls, 737 Map<RoutineIdentity, List<String>> localReasons, 738 Map<RoutineIdentity, RoutineSummary> evaluated) { 739 Map<RoutineIdentity, Set<String>> reasons = 740 new LinkedHashMap<RoutineIdentity, Set<String>>(); 741 for (RoutineIdentity id : component) { 742 Set<String> own = new TreeSet<String>(); 743 List<String> local = localReasons.get(id); 744 if (local != null) { 745 own.addAll(local); 746 } 747 List<SiteBinding> calls = frozenCalls.get(id); 748 if (calls != null) { 749 for (SiteBinding call : calls) { 750 RoutineIdentity callee = call.bound.getResult().getResolved(); 751 if (!members.contains(callee)) { 752 RoutineSummary done = evaluated.get(callee); 753 if (done != null && done.getCompleteness() 754 != RoutineSummary.Completeness.COMPLETE) { 755 own.add("CALLEE_PARTIAL:" + callee.signatureKey()); 756 } 757 } 758 } 759 } 760 reasons.put(id, own); 761 } 762 boolean changed; 763 do { 764 changed = false; 765 for (RoutineIdentity id : component) { 766 List<SiteBinding> calls = frozenCalls.get(id); 767 if (calls == null) { 768 continue; 769 } 770 for (SiteBinding call : calls) { 771 RoutineIdentity callee = call.bound.getResult().getResolved(); 772 if (members.contains(callee) 773 && !reasons.get(callee).isEmpty()) { 774 changed |= reasons.get(id).add( 775 "CALLEE_PARTIAL:" + callee.signatureKey()); 776 } 777 } 778 } 779 } while (changed); 780 return reasons; 781 } 782 783 private Set<RoutineSummaryEdge> composeSummaryEdges(RoutineSummary base, 784 RoutineIdentity callerId, List<SiteBinding> calls) { 785 Map<ComposedEdge, ComposedEdge> composed = 786 new LinkedHashMap<ComposedEdge, ComposedEdge>(); 787 for (SiteBinding call : calls) { 788 composeCallSite(call.site, call.bound, call.callee, callerId, 789 call.ordinal, call.resultProjectionProven, composed); 790 } 791 composeBridges(callerId, calls, composed); 792 Map<RoutineSummaryEdge, RoutineSummaryEdge> joined = 793 new LinkedHashMap<RoutineSummaryEdge, RoutineSummaryEdge>(); 794 for (RoutineSummaryEdge edge : base.getEdges()) { 795 joinSummaryEdge(joined, edge); 796 } 797 for (ComposedEdge edge : composed.values()) { 798 joinSummaryEdge(joined, new RoutineSummaryEdge(edge.getSource(), 799 edge.getTarget(), edge.getRelationType(), 800 edge.getObservedFunctions())); 801 } 802 List<RoutineSummaryEdge> ordered = 803 new ArrayList<RoutineSummaryEdge>(joined.values()); 804 Collections.sort(ordered, SUMMARY_EDGE_ORDER); 805 return new LinkedHashSet<RoutineSummaryEdge>(ordered); 806 } 807 808 private void joinSummaryEdge( 809 Map<RoutineSummaryEdge, RoutineSummaryEdge> joined, 810 RoutineSummaryEdge candidate) { 811 RoutineSummaryEdge bounded = boundObservedFunctions(candidate); 812 RoutineSummaryEdge prior = joined.get(bounded); 813 joined.put(bounded, prior == null ? bounded 814 : prior.joinMetadata(bounded, 815 options.getMaxObservedFunctions())); 816 } 817 818 private RoutineSummaryEdge boundObservedFunctions( 819 RoutineSummaryEdge edge) { 820 if (edge.isObservedFunctionsTop() 821 || edge.getObservedFunctions().size() 822 <= options.getMaxObservedFunctions()) { 823 return edge; 824 } 825 return new RoutineSummaryEdge(edge.getSource(), edge.getTarget(), 826 edge.getRelationType(), Collections.singleton( 827 RoutineSummaryEdge.OBSERVED_FUNCTIONS_TOP)); 828 } 829 830 private static boolean sameSummaryValues( 831 Map<RoutineIdentity, RoutineSummary> a, 832 Map<RoutineIdentity, RoutineSummary> b) { 833 if (!a.keySet().equals(b.keySet())) { 834 return false; 835 } 836 for (RoutineIdentity id : a.keySet()) { 837 RoutineSummary left = a.get(id); 838 RoutineSummary right = b.get(id); 839 if (left.getCompleteness() != right.getCompleteness() 840 || !left.getCompletenessReasons().equals( 841 right.getCompletenessReasons()) 842 || !sameEdgeValues(left.getEdges(), right.getEdges())) { 843 return false; 844 } 845 } 846 return true; 847 } 848 849 private static boolean sameEdgeValues(Set<RoutineSummaryEdge> a, 850 Set<RoutineSummaryEdge> b) { 851 if (a.size() != b.size()) { 852 return false; 853 } 854 Map<RoutineSummaryEdge, Set<String>> rightValues = 855 new HashMap<RoutineSummaryEdge, Set<String>>(); 856 for (RoutineSummaryEdge edge : b) { 857 rightValues.put(edge, edge.getObservedFunctions()); 858 } 859 for (RoutineSummaryEdge edge : a) { 860 Set<String> right = rightValues.get(edge); 861 if (right == null 862 || !edge.getObservedFunctions().equals(right)) { 863 return false; 864 } 865 } 866 return true; 867 } 868 869 private static List<SiteBinding> callsForEvaluation( 870 List<SiteBinding> frozen, Map<RoutineIdentity, RoutineSummary> current, 871 Map<RoutineIdentity, RoutineSummary> evaluated) { 872 List<SiteBinding> result = new ArrayList<SiteBinding>(); 873 if (frozen == null) { 874 return result; 875 } 876 for (SiteBinding call : frozen) { 877 RoutineIdentity calleeId = call.bound.getResult().getResolved(); 878 RoutineSummary callee = current.get(calleeId); 879 if (callee == null) { 880 callee = evaluated.get(calleeId); 881 } 882 if (callee != null) { 883 result.add(call.withCallee(callee)); 884 } 885 } 886 return result; 887 } 888 889 private static List<SiteBinding> callsWithEvaluatedCallees( 890 List<SiteBinding> frozen, 891 Map<RoutineIdentity, RoutineSummary> evaluated) { 892 return callsForEvaluation(frozen, 893 Collections.<RoutineIdentity, RoutineSummary>emptyMap(), evaluated); 894 } 895 896 // ------------------------------------------------------------------ 897 // Deterministic non-recursive SCC discovery and callee-first ordering. 898 // ------------------------------------------------------------------ 899 900 private static final Comparator<RoutineIdentity> IDENTITY_ORDER = 901 new Comparator<RoutineIdentity>() { 902 @Override 903 public int compare(RoutineIdentity a, RoutineIdentity b) { 904 return a.signatureKey().compareTo(b.signatureKey()); 905 } 906 }; 907 908 private static final Comparator<RoutineSummaryEdge> SUMMARY_EDGE_ORDER = 909 new Comparator<RoutineSummaryEdge>() { 910 @Override 911 public int compare(RoutineSummaryEdge a, RoutineSummaryEdge b) { 912 int c = a.getSource().compareTo(b.getSource()); 913 if (c != 0) return c; 914 c = a.getTarget().compareTo(b.getTarget()); 915 if (c != 0) return c; 916 return a.getRelationType().compareTo(b.getRelationType()); 917 } 918 }; 919 920 private static List<RoutineIdentity> orderedIdentities( 921 Set<RoutineIdentity> identities) { 922 List<RoutineIdentity> ordered = new ArrayList<RoutineIdentity>(identities); 923 Collections.sort(ordered, IDENTITY_ORDER); 924 return ordered; 925 } 926 927 private static final class DfsFrame { 928 final RoutineIdentity node; 929 final List<RoutineIdentity> neighbors; 930 int next; 931 932 DfsFrame(RoutineIdentity node, List<RoutineIdentity> neighbors) { 933 this.node = node; 934 this.neighbors = neighbors; 935 } 936 } 937 938 private static List<List<RoutineIdentity>> calleeFirstComponents( 939 Set<RoutineIdentity> nodes, 940 Map<RoutineIdentity, List<SiteBinding>> calls) { 941 Map<RoutineIdentity, List<RoutineIdentity>> graph = 942 adjacency(nodes, calls, false); 943 Map<RoutineIdentity, List<RoutineIdentity>> reverse = 944 adjacency(nodes, calls, true); 945 Set<RoutineIdentity> visited = new HashSet<RoutineIdentity>(); 946 List<RoutineIdentity> finish = new ArrayList<RoutineIdentity>(); 947 for (RoutineIdentity start : orderedIdentities(nodes)) { 948 if (!visited.add(start)) { 949 continue; 950 } 951 Deque<DfsFrame> stack = new ArrayDeque<DfsFrame>(); 952 stack.push(new DfsFrame(start, graph.get(start))); 953 while (!stack.isEmpty()) { 954 DfsFrame frame = stack.peek(); 955 if (frame.next < frame.neighbors.size()) { 956 RoutineIdentity next = frame.neighbors.get(frame.next++); 957 if (visited.add(next)) { 958 stack.push(new DfsFrame(next, graph.get(next))); 959 } 960 } else { 961 finish.add(frame.node); 962 stack.pop(); 963 } 964 } 965 } 966 List<List<RoutineIdentity>> components = 967 new ArrayList<List<RoutineIdentity>>(); 968 visited.clear(); 969 for (int i = finish.size() - 1; i >= 0; i--) { 970 RoutineIdentity start = finish.get(i); 971 if (!visited.add(start)) { 972 continue; 973 } 974 List<RoutineIdentity> component = new ArrayList<RoutineIdentity>(); 975 Deque<RoutineIdentity> stack = new ArrayDeque<RoutineIdentity>(); 976 stack.push(start); 977 while (!stack.isEmpty()) { 978 RoutineIdentity node = stack.pop(); 979 component.add(node); 980 List<RoutineIdentity> neighbors = reverse.get(node); 981 for (int n = neighbors.size() - 1; n >= 0; n--) { 982 if (visited.add(neighbors.get(n))) { 983 stack.push(neighbors.get(n)); 984 } 985 } 986 } 987 Collections.sort(component, IDENTITY_ORDER); 988 components.add(component); 989 } 990 return orderComponentsByDependencies(components, graph); 991 } 992 993 private static Map<RoutineIdentity, List<RoutineIdentity>> adjacency( 994 Set<RoutineIdentity> nodes, 995 Map<RoutineIdentity, List<SiteBinding>> calls, boolean reverse) { 996 Map<RoutineIdentity, Set<RoutineIdentity>> sets = 997 new LinkedHashMap<RoutineIdentity, Set<RoutineIdentity>>(); 998 for (RoutineIdentity id : orderedIdentities(nodes)) { 999 sets.put(id, new TreeSet<RoutineIdentity>(IDENTITY_ORDER)); 1000 } 1001 for (RoutineIdentity caller : orderedIdentities(nodes)) { 1002 List<SiteBinding> sites = calls.get(caller); 1003 if (sites == null) { 1004 continue; 1005 } 1006 for (SiteBinding site : sites) { 1007 RoutineIdentity callee = site.bound.getResult().getResolved(); 1008 if (!sets.containsKey(callee)) { 1009 continue; 1010 } 1011 if (reverse) { 1012 sets.get(callee).add(caller); 1013 } else { 1014 sets.get(caller).add(callee); 1015 } 1016 } 1017 } 1018 Map<RoutineIdentity, List<RoutineIdentity>> result = 1019 new LinkedHashMap<RoutineIdentity, List<RoutineIdentity>>(); 1020 for (RoutineIdentity id : orderedIdentities(nodes)) { 1021 result.put(id, new ArrayList<RoutineIdentity>(sets.get(id))); 1022 } 1023 return result; 1024 } 1025 1026 private static List<List<RoutineIdentity>> orderComponentsByDependencies( 1027 List<List<RoutineIdentity>> components, 1028 Map<RoutineIdentity, List<RoutineIdentity>> graph) { 1029 Map<RoutineIdentity, Integer> componentOf = 1030 new HashMap<RoutineIdentity, Integer>(); 1031 for (int i = 0; i < components.size(); i++) { 1032 for (RoutineIdentity id : components.get(i)) { 1033 componentOf.put(id, Integer.valueOf(i)); 1034 } 1035 } 1036 Map<Integer, Set<Integer>> dependencies = 1037 new LinkedHashMap<Integer, Set<Integer>>(); 1038 for (int i = 0; i < components.size(); i++) { 1039 dependencies.put(Integer.valueOf(i), new LinkedHashSet<Integer>()); 1040 } 1041 for (Map.Entry<RoutineIdentity, List<RoutineIdentity>> entry 1042 : graph.entrySet()) { 1043 int caller = componentOf.get(entry.getKey()).intValue(); 1044 for (RoutineIdentity calleeId : entry.getValue()) { 1045 int callee = componentOf.get(calleeId).intValue(); 1046 if (caller != callee) { 1047 dependencies.get(Integer.valueOf(caller)).add( 1048 Integer.valueOf(callee)); 1049 } 1050 } 1051 } 1052 List<List<RoutineIdentity>> ordered = 1053 new ArrayList<List<RoutineIdentity>>(); 1054 Set<Integer> emitted = new HashSet<Integer>(); 1055 while (ordered.size() < components.size()) { 1056 int winner = -1; 1057 String winnerKey = null; 1058 for (int i = 0; i < components.size(); i++) { 1059 if (emitted.contains(Integer.valueOf(i)) 1060 || !emitted.containsAll(dependencies.get(Integer.valueOf(i)))) { 1061 continue; 1062 } 1063 String key = componentKey(components.get(i)); 1064 if (winner < 0 || key.compareTo(winnerKey) < 0) { 1065 winner = i; 1066 winnerKey = key; 1067 } 1068 } 1069 if (winner < 0) { 1070 throw new IllegalStateException("SCC condensation graph is cyclic"); 1071 } 1072 emitted.add(Integer.valueOf(winner)); 1073 ordered.add(components.get(winner)); 1074 } 1075 return ordered; 1076 } 1077 1078 private static String componentKey(List<RoutineIdentity> component) { 1079 StringBuilder key = new StringBuilder(); 1080 for (RoutineIdentity id : component) { 1081 if (key.length() > 0) { 1082 key.append(','); 1083 } 1084 key.append(id.signatureKey()); 1085 } 1086 return key.toString(); 1087 } 1088 1089 private static Set<ComposedEdge> orderedAdditions( 1090 Map<ComposedEdge, ComposedEdge> additions) { 1091 List<ComposedEdge> ordered = 1092 new ArrayList<ComposedEdge>(additions.values()); 1093 Collections.sort(ordered, new Comparator<ComposedEdge>() { 1094 @Override 1095 public int compare(ComposedEdge a, ComposedEdge b) { 1096 int c = a.getSource().compareTo(b.getSource()); 1097 if (c != 0) return c; 1098 c = a.getTarget().compareTo(b.getTarget()); 1099 if (c != 0) return c; 1100 c = a.getRelationType().compareTo(b.getRelationType()); 1101 if (c != 0) return c; 1102 c = IDENTITY_ORDER.compare(a.getCallerIdentity(), 1103 b.getCallerIdentity()); 1104 if (c != 0) return c; 1105 c = IDENTITY_ORDER.compare(a.getCalleeIdentity(), 1106 b.getCalleeIdentity()); 1107 if (c != 0) return c; 1108 return a.getCallSiteOrdinal() - b.getCallSiteOrdinal(); 1109 } 1110 }); 1111 return new LinkedHashSet<ComposedEdge>(ordered); 1112 } 1113 1114 /** 1115 * Intra-caller call-to-call bridge (codex-B3-r1 finding 5, generalized 1116 * to a FIXED POINT per codex-B3-r2 finding 5): for 1117 * {@code A(@v OUTPUT); B(@v)} the local {@code @v} is interior on both 1118 * sides, so neither call site's BOUNDARY reach carries the flow — the 1119 * node-level maps do. The inline engine's shared variable node merges 1120 * all reads/writes flow-INsensitively, so no statement-order check 1121 * belongs here, and the SAME call site may bridge to itself 1122 * ({@code P(@v, @v OUTPUT)}). 1123 * 1124 * <p>Mechanics: seed node-level FACTS (external sources written by a 1125 * callee OUT projection onto every model node the lvalue forward- 1126 * reaches), then propagate: an IN actual whose node carries facts feeds 1127 * its callee's formal-sourced edges — table/temp targets emit 1128 * additions, result-set targets map through INSERT…EXEC, and OUT-formal 1129 * targets seed NEW facts (the chain hop). Facts are monotone bit-joins 1130 * ⇒ the loop converges; the guard is a safety valve only. 1131 */ 1132 private void composeBridges(RoutineIdentity callerIdentity, 1133 List<SiteBinding> resolved, 1134 Map<ComposedEdge, ComposedEdge> additions) { 1135 // model node -> external source endpoint -> flavor bits 1136 // (1 = an all-fdd chain reaches this node, 2 = an fdr-bearing one) 1137 Map<String, Map<Endpoint, Integer>> nodeFacts = 1138 new LinkedHashMap<String, Map<Endpoint, Integer>>(); 1139 // GENUINE convergence, no iteration cap (codex-B3-r3 finding 1: a 1140 // sites-based cap cut long value/filter cycles short and silently 1141 // dropped fdr edges). Termination is structural: every state change 1142 // is a monotone join into a finite space — fact flavor bits per 1143 // (node, source) and the additions set — so a full sweep with no 1144 // change is reached in finitely many sweeps. 1145 boolean changed = seedOutFacts(callerIdentity, resolved, nodeFacts); 1146 while (changed) { 1147 changed = false; 1148 for (SiteBinding in : resolved) { 1149 RoutineIdentity inCallee = in.bound.getResult().getResolved(); 1150 for (Map.Entry<Integer, Set<String>> inEntry 1151 : in.site.getInNodes().entrySet()) { 1152 int inActual = inEntry.getKey(); 1153 Map<Endpoint, Integer> facts = factsAt(nodeFacts, 1154 inEntry.getValue()); 1155 if (facts.isEmpty()) { 1156 continue; 1157 } 1158 int inFormal = formalOfActual( 1159 in.bound.getActualPositionByFormal(), inActual); 1160 if (inFormal < 0 || !in.bound.getResult().getInputs() 1161 .containsKey(inFormal)) { 1162 continue; // binder refused the input (literal, pure OUT) 1163 } 1164 for (RoutineSummaryEdge edge : in.callee.getEdges()) { 1165 if (edge.getSource().getKind() != EndpointKind.FORMAL 1166 || formalPositionOf(edge.getSource(), inCallee) 1167 != inFormal) { 1168 continue; 1169 } 1170 boolean edgeFdr = "fdr".equals(edge.getRelationType()); // non-identifier-compare: relation type token 1171 changed |= emitOrChain(in, edge, edgeFdr, facts, 1172 callerIdentity, inCallee, additions, nodeFacts); 1173 } 1174 } 1175 } 1176 } 1177 } 1178 1179 /** Seed facts: each validated OUT projection writes its callee-side 1180 * sources onto every node the bound lvalue forward-reaches. Returns 1181 * true when anything was seeded. */ 1182 private boolean seedOutFacts(RoutineIdentity callerIdentity, 1183 List<SiteBinding> resolved, 1184 Map<String, Map<Endpoint, Integer>> nodeFacts) { 1185 boolean seeded = false; 1186 for (SiteBinding out : resolved) { 1187 RoutineIdentity outCallee = out.bound.getResult().getResolved(); 1188 for (Map.Entry<Integer, RoutineCallBinder.WritableTarget> o 1189 : out.bound.getResult().getOutputs().entrySet()) { 1190 int outFormal = o.getKey(); 1191 Integer outActual = out.bound.getActualPositionByFormal().get(outFormal); 1192 if (outActual == null) { 1193 continue; 1194 } 1195 Map<String, Integer> reachNodes = 1196 out.site.getOutReachNodes().get(outActual); 1197 if (reachNodes == null || reachNodes.isEmpty()) { 1198 continue; 1199 } 1200 for (RoutineSummaryEdge edge : out.callee.getEdges()) { 1201 if (edge.getTarget().getKind() != EndpointKind.FORMAL 1202 || formalPositionOf(edge.getTarget(), outCallee) 1203 != outFormal) { 1204 continue; 1205 } 1206 boolean edgeFdr = "fdr".equals(edge.getRelationType()); // non-identifier-compare: relation type token 1207 for (Attributed s : resolveCalleeSource(edge.getSource(), 1208 outCallee, out.bound.getActualPositionByFormal(), 1209 out.site, out.bound.getResult(), callerIdentity)) { 1210 for (Map.Entry<String, Integer> node : reachNodes.entrySet()) { 1211 int bits = flavorOf(s.fdr || edgeFdr, node.getValue()); 1212 seeded |= joinFact(nodeFacts, node.getKey(), 1213 s.endpoint, bits); 1214 } 1215 } 1216 } 1217 } 1218 } 1219 return seeded; 1220 } 1221 1222 /** Emit additions for one fact-fed callee edge, or seed chained facts 1223 * when the edge lands on a further OUT projection. */ 1224 private boolean emitOrChain(SiteBinding in, RoutineSummaryEdge edge, 1225 boolean edgeFdr, Map<Endpoint, Integer> facts, 1226 RoutineIdentity callerIdentity, RoutineIdentity inCallee, 1227 Map<ComposedEdge, ComposedEdge> additions, 1228 Map<String, Map<Endpoint, Integer>> nodeFacts) { 1229 boolean changed = false; 1230 Endpoint target = edge.getTarget(); 1231 if (target.getKind() == EndpointKind.TABLE_COLUMN 1232 || target.getKind() == EndpointKind.TEMP_COLUMN 1233 || target.getKind() == EndpointKind.RESULT_SET_COLUMN) { 1234 List<Attributed> resolvedTargets = resolveCalleeTarget(target, 1235 inCallee, in.bound.getActualPositionByFormal(), in.site, 1236 in.bound, in.callee, in.resultProjectionProven); 1237 for (Attributed t : resolvedTargets) { 1238 for (Map.Entry<Endpoint, Integer> fact : facts.entrySet()) { 1239 boolean legsFdd = !edgeFdr && !t.fdr; 1240 if (legsFdd && (fact.getValue() & 1) != 0) { 1241 changed |= addComposedEdge(additions, fact.getKey(), 1242 t.endpoint, "fdd", callerIdentity, inCallee, 1243 in.ordinal, edge.getObservedFunctions(), 1244 // A node bridge may already contain effects 1245 // from another callee whose completeness is 1246 // not represented in the legacy bitset. Keep 1247 // the gate conservative until bridge facts 1248 // carry full provenance. 1249 RoutineSummary.Completeness.PARTIAL); 1250 } 1251 if (!legsFdd || (fact.getValue() & 2) != 0) { 1252 changed |= addComposedEdge(additions, fact.getKey(), 1253 t.endpoint, "fdr", callerIdentity, inCallee, 1254 in.ordinal, edge.getObservedFunctions(), 1255 RoutineSummary.Completeness.PARTIAL); 1256 } 1257 } 1258 } 1259 return changed; 1260 } 1261 if (target.getKind() == EndpointKind.FORMAL) { 1262 // chain hop: IN formal -> OUT formal of the same callee, whose 1263 // lvalue seeds facts further downstream 1264 int chainedFormal = formalPositionOf(target, inCallee); 1265 if (chainedFormal < 0 || !in.bound.getResult().getOutputs() 1266 .containsKey(chainedFormal)) { 1267 return false; 1268 } 1269 Integer chainedActual = in.bound.getActualPositionByFormal() 1270 .get(chainedFormal); 1271 if (chainedActual == null) { 1272 return false; 1273 } 1274 Map<String, Integer> reachNodes = 1275 in.site.getOutReachNodes().get(chainedActual); 1276 if (reachNodes == null) { 1277 return false; 1278 } 1279 for (Map.Entry<Endpoint, Integer> fact : facts.entrySet()) { 1280 for (Map.Entry<String, Integer> node : reachNodes.entrySet()) { 1281 boolean fdrChain = edgeFdr || (fact.getValue() & 1) == 0; 1282 int bits = flavorOf(fdrChain, node.getValue()); 1283 // an fdr-bearing incoming fact also carries fdr onward 1284 if ((fact.getValue() & 2) != 0) { 1285 bits |= 2; 1286 } 1287 changed |= joinFact(nodeFacts, node.getKey(), 1288 fact.getKey(), bits); 1289 } 1290 } 1291 } 1292 return changed; 1293 } 1294 1295 /** Compose one leg's fdr-ness with a bridge segment's flavor bits. */ 1296 private static int flavorOf(boolean legFdr, int pathBits) { 1297 int bits = 0; 1298 if (!legFdr && (pathBits & 1) != 0) { 1299 bits |= 1; 1300 } 1301 if (legFdr || (pathBits & 2) != 0) { 1302 bits |= 2; 1303 } 1304 return bits; 1305 } 1306 1307 private static boolean joinFact(Map<String, Map<Endpoint, Integer>> nodeFacts, 1308 String node, Endpoint source, int bits) { 1309 if (bits == 0) { 1310 return false; 1311 } 1312 Map<Endpoint, Integer> facts = nodeFacts.get(node); 1313 if (facts == null) { 1314 facts = new LinkedHashMap<Endpoint, Integer>(); 1315 nodeFacts.put(node, facts); 1316 } 1317 Integer prior = facts.get(source); 1318 int joined = prior == null ? bits : (prior | bits); 1319 if (prior != null && joined == prior) { 1320 return false; 1321 } 1322 facts.put(source, joined); 1323 return true; 1324 } 1325 1326 /** Union of facts across an actual's own model nodes. */ 1327 private static Map<Endpoint, Integer> factsAt( 1328 Map<String, Map<Endpoint, Integer>> nodeFacts, Set<String> nodes) { 1329 Map<Endpoint, Integer> union = new LinkedHashMap<Endpoint, Integer>(); 1330 for (String node : nodes) { 1331 Map<Endpoint, Integer> facts = nodeFacts.get(node); 1332 if (facts == null) { 1333 continue; 1334 } 1335 for (Map.Entry<Endpoint, Integer> e : facts.entrySet()) { 1336 Integer prior = union.get(e.getKey()); 1337 union.put(e.getKey(), prior == null 1338 ? e.getValue() : (prior | e.getValue())); 1339 } 1340 } 1341 return union; 1342 } 1343 1344 /** Formal position bound to a given actual position; -1 when none. */ 1345 private static int formalOfActual(Map<Integer, Integer> actualByFormal, 1346 int actualPos) { 1347 for (Map.Entry<Integer, Integer> e : actualByFormal.entrySet()) { 1348 if (e.getValue() == actualPos) { 1349 return e.getKey(); 1350 } 1351 } 1352 return -1; 1353 } 1354 1355 /** Compose one RESOLVED call site: callee summary edges × caller reach. */ 1356 private void composeCallSite(CallSiteRef site, 1357 CatalogRoutineCallBinder.BoundCall bound, RoutineSummary callee, 1358 RoutineIdentity callerIdentity, int callSiteOrdinal, 1359 boolean resultProjectionProven, 1360 Map<ComposedEdge, ComposedEdge> additions) { 1361 RoutineIdentity calleeId = bound.getResult().getResolved(); 1362 // Public B6 is observation-only until call validity, type/mutability, 1363 // context, inventory, and every projection shape are exhaustively 1364 // preflighted. Completeness cannot authorize application here. 1365 RoutineSummary.Completeness effectiveCompleteness = 1366 RoutineSummary.Completeness.PARTIAL; 1367 Map<Integer, Integer> actualByFormal = bound.getActualPositionByFormal(); 1368 for (RoutineSummaryEdge edge : callee.getEdges()) { 1369 boolean edgeFdr = "fdr".equals(edge.getRelationType()); // non-identifier-compare: relation type token 1370 // Resolve the callee edge's SOURCE into caller space. 1371 List<Attributed> sources = resolveCalleeSource(edge.getSource(), 1372 calleeId, actualByFormal, site, bound.getResult(), 1373 callerIdentity); 1374 if (sources.isEmpty()) { 1375 continue; 1376 } 1377 // Resolve the callee edge's TARGET into caller space. 1378 List<Attributed> targets = resolveCalleeTarget(edge.getTarget(), 1379 calleeId, actualByFormal, site, bound, callee, 1380 resultProjectionProven); 1381 1382 for (Attributed s : sources) { 1383 for (Attributed t : targets) { 1384 boolean anyFdr = edgeFdr || s.fdr || t.fdr; 1385 boolean allFdd = !edgeFdr && !s.fdr && !t.fdr; 1386 if (allFdd) { 1387 addComposedEdge(additions, s.endpoint, t.endpoint, 1388 "fdd", callerIdentity, calleeId, callSiteOrdinal, 1389 edge.getObservedFunctions(), effectiveCompleteness); 1390 } else if (anyFdr) { 1391 addComposedEdge(additions, s.endpoint, t.endpoint, 1392 "fdr", callerIdentity, calleeId, callSiteOrdinal, 1393 edge.getObservedFunctions(), effectiveCompleteness); 1394 } 1395 } 1396 } 1397 } 1398 } 1399 1400 /** Add one occurrence edge, joining bounded observed-function metadata when the 1401 * same occurrence is reached through more than one summary path. */ 1402 private boolean addComposedEdge( 1403 Map<ComposedEdge, ComposedEdge> additions, 1404 Endpoint source, Endpoint target, String relationType, 1405 RoutineIdentity callerIdentity, RoutineIdentity calleeIdentity, 1406 int callSiteOrdinal, Set<String> observedFunctions, 1407 RoutineSummary.Completeness completeness) { 1408 ComposedEdge candidate = new ComposedEdge(source, target, relationType, 1409 callerIdentity, calleeIdentity, callSiteOrdinal, 1410 joinObservedFunctions(Collections.<String>emptySet(), 1411 observedFunctions), 1412 completeness); 1413 ComposedEdge prior = additions.get(candidate); 1414 if (prior == null) { 1415 additions.put(candidate, candidate); 1416 return true; 1417 } 1418 Set<String> joined = joinObservedFunctions( 1419 prior.getObservedFunctions(), observedFunctions); 1420 RoutineSummary.Completeness joinedCompleteness = 1421 prior.getCalleeCompleteness() == RoutineSummary.Completeness.COMPLETE 1422 && completeness == RoutineSummary.Completeness.COMPLETE 1423 ? RoutineSummary.Completeness.COMPLETE 1424 : RoutineSummary.Completeness.PARTIAL; 1425 if (joined.equals(prior.getObservedFunctions()) 1426 && joinedCompleteness == prior.getCalleeCompleteness()) { 1427 return false; 1428 } 1429 additions.put(candidate, 1430 prior.withMetadata(joined, joinedCompleteness)); 1431 return true; 1432 } 1433 1434 private Set<String> joinObservedFunctions(Set<String> left, 1435 Set<String> right) { 1436 if (left.contains(RoutineSummaryEdge.OBSERVED_FUNCTIONS_TOP) 1437 || (right != null 1438 && right.contains( 1439 RoutineSummaryEdge.OBSERVED_FUNCTIONS_TOP))) { 1440 return Collections.singleton( 1441 RoutineSummaryEdge.OBSERVED_FUNCTIONS_TOP); 1442 } 1443 Set<String> joined = new TreeSet<String>(left); 1444 if (right != null) { 1445 joined.addAll(right); 1446 } 1447 if (joined.size() > options.getMaxObservedFunctions()) { 1448 return Collections.singleton( 1449 RoutineSummaryEdge.OBSERVED_FUNCTIONS_TOP); 1450 } 1451 return joined; 1452 } 1453 1454 /** A caller-space endpoint plus the relation flavor of the leg that 1455 * produced it (fdr=true when the leg passed through an fdr hop). */ 1456 private static final class Attributed { 1457 final Endpoint endpoint; 1458 final boolean fdr; 1459 1460 Attributed(Endpoint endpoint, boolean fdr) { 1461 this.endpoint = endpoint; 1462 this.fdr = fdr; 1463 } 1464 } 1465 1466 /** 1467 * Callee edge SOURCE in caller space: a formal chains through the caller 1468 * sources of its bound actual; global endpoints (tables, temps) pass 1469 * through unchanged. Result-set sources never compose (a callee cannot 1470 * read its own output shape). 1471 */ 1472 private List<Attributed> resolveCalleeSource(Endpoint source, 1473 RoutineIdentity calleeId, Map<Integer, Integer> actualByFormal, 1474 CallSiteRef site, RoutineCallBinder.BindingResult binding, 1475 RoutineIdentity callerId) { 1476 List<Attributed> result = new ArrayList<Attributed>(); 1477 if (source.getKind() == EndpointKind.FORMAL) { 1478 int formal = formalPositionOf(source, calleeId); 1479 Integer actualPos = formal < 0 ? null : actualByFormal.get(formal); 1480 if (actualPos == null) { 1481 return result; // defaulted formal: no caller-side source 1482 } 1483 // The binder's INPUT map is the authority: a formal it refused 1484 // to bind (Oracle pure OUT, literal) contributes no IN leg — 1485 // composing the raw reach anyway would fabricate flow 1486 // (codex-B3-r2 finding 3). 1487 if (!binding.getInputs().containsKey(formal)) { 1488 return result; 1489 } 1490 addAll(result, site.getInSourcesFdd().get(actualPos), false); 1491 addAll(result, site.getInSourcesFdr().get(actualPos), true); 1492 if (result.isEmpty()) { 1493 Endpoint callerFormal = callerFormalEndpoint(site, actualPos, 1494 callerId); 1495 if (callerFormal != null) { 1496 result.add(new Attributed(callerFormal, false)); 1497 } 1498 } 1499 return result; 1500 } 1501 if (source.getKind() == EndpointKind.TABLE_COLUMN 1502 || source.getKind() == EndpointKind.TEMP_COLUMN) { 1503 result.add(new Attributed(source, false)); 1504 } 1505 return result; 1506 } 1507 1508 /** 1509 * Identity-backed fallback for a bare actual that is itself one of the 1510 * caller's formals. Some dlineage models contain two variable nodes for 1511 * that spelling and the call-site reach collector can select the interior 1512 * one, leaving its boundary map empty. The declaration identity proves 1513 * this exact formal endpoint; no heuristic first-variable link is used. 1514 */ 1515 private Endpoint callerFormalEndpoint(CallSiteRef site, int actualPos, 1516 RoutineIdentity callerId) { 1517 if (callerId == null || actualPos < 0 1518 || actualPos >= site.getActuals().size()) { 1519 return null; 1520 } 1521 CallSiteRef.Actual actual = site.getActuals().get(actualPos); 1522 if (actual.getBareReference() == null) { 1523 return null; 1524 } 1525 String actualName = actual.getBareReference().toString(); 1526 List<String> segments = gudusoft.gsqlparser.util.SQLUtil.parseNames(actualName); 1527 // This fallback has no symbol identity, so it is safe only for an 1528 // unqualified spelling. A record field such as rec.p_in must use the 1529 // model reach collected for that exact object; suffix-matching it to 1530 // formal p_in would fabricate a caller-formal source. 1531 if (segments.size() != 1) { 1532 return null; 1533 } 1534 actualName = segments.get(0); 1535 for (String declared : callerId.getRawParamNames()) { 1536 String actualCompare = actualName.startsWith("@") 1537 ? actualName.substring(1) : actualName; 1538 String declaredCompare = declared.startsWith("@") 1539 ? declared.substring(1) : declared; 1540 if (gudusoft.gsqlparser.util.SQLUtil.sameName(vendor, 1541 gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, 1542 actualCompare, declaredCompare)) { 1543 return new Endpoint(EndpointKind.FORMAL, declared, declared); 1544 } 1545 } 1546 return null; 1547 } 1548 1549 /** 1550 * Callee edge TARGET in caller space: an OUT formal projects onto the 1551 * caller boundary reachable from its bound lvalue; a result-set column 1552 * maps positionally onto INSERT…EXEC targets; global endpoints pass 1553 * through unchanged. 1554 */ 1555 private List<Attributed> resolveCalleeTarget(Endpoint target, 1556 RoutineIdentity calleeId, Map<Integer, Integer> actualByFormal, 1557 CallSiteRef site, CatalogRoutineCallBinder.BoundCall bound, 1558 RoutineSummary callee, boolean resultProjectionProven) { 1559 List<Attributed> result = new ArrayList<Attributed>(); 1560 if (target.getKind() == EndpointKind.FORMAL) { 1561 int formal = formalPositionOf(target, calleeId); 1562 if (formal < 0 1563 || !bound.getResult().getOutputs().containsKey(formal)) { 1564 return result; // not a validated OUT projection — nothing composes 1565 } 1566 Integer actualPos = actualByFormal.get(formal); 1567 if (actualPos == null) { 1568 return result; 1569 } 1570 addAll(result, site.getOutTargetsFdd().get(actualPos), false); 1571 addAll(result, site.getOutTargetsFdr().get(actualPos), true); 1572 return result; 1573 } 1574 if (target.getKind() == EndpointKind.RESULT_SET_COLUMN) { 1575 if (!resultProjectionProven) { 1576 return result; 1577 } 1578 if (site.getInsertTargets() == null) { 1579 return result; 1580 } 1581 List<String> shape = callee.getResultSetShapes().get(target.getParent()); 1582 if (shape == null) { 1583 return result; 1584 } 1585 // SQL Server rejects INSERT...EXEC when result and target 1586 // cardinalities differ. Mapping a matching prefix would publish 1587 // lineage for a statement that cannot produce that write. 1588 if (shape.size() != site.getInsertTargets().size()) { 1589 return result; 1590 } 1591 int ordinal = uniqueShapeOrdinal(shape, target.getColumn()); 1592 if (ordinal < 0 || ordinal >= site.getInsertTargets().size()) { 1593 return result; 1594 } 1595 result.add(new Attributed(site.getInsertTargets().get(ordinal), false)); 1596 return result; 1597 } 1598 if (target.getKind() == EndpointKind.TABLE_COLUMN 1599 || target.getKind() == EndpointKind.TEMP_COLUMN) { 1600 result.add(new Attributed(target, false)); 1601 } 1602 return result; 1603 } 1604 1605 /** Exact positional lookup only when the output alias occurs once under 1606 * the vendor's identifier equality. Zero or duplicate matches are 1607 * deferred; selecting the first duplicate would fabricate an ordinal. */ 1608 private int uniqueShapeOrdinal(List<String> shape, String columnName) { 1609 if (hasAmbiguousShapeAliases(shape)) { 1610 return -1; 1611 } 1612 int found = -1; 1613 for (int i = 0; i < shape.size(); i++) { 1614 if (gudusoft.gsqlparser.util.SQLUtil.sameName(vendor, 1615 gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, 1616 shape.get(i), columnName)) { 1617 if (found >= 0) { 1618 return -1; 1619 } 1620 found = i; 1621 } 1622 } 1623 return found; 1624 } 1625 1626 /** dlineage occurrence-disambiguates duplicate result aliases as 1627 * {@code name(1)}, {@code name(2)}, ... . Treat both literal duplicates 1628 * and that generated suffix form as ambiguous. A conservative false 1629 * deferral of a deliberately parenthesized alias is safer than selecting 1630 * a fabricated ordinal. */ 1631 private boolean hasAmbiguousShapeAliases(List<String> shape) { 1632 for (int i = 0; i < shape.size(); i++) { 1633 for (int j = i + 1; j < shape.size(); j++) { 1634 if (sameShapeColumn(shape.get(i), shape.get(j))) { 1635 return true; 1636 } 1637 } 1638 String base = syntheticDuplicateBase(shape.get(i)); 1639 if (base != null) { 1640 for (int j = 0; j < shape.size(); j++) { 1641 if (j != i && sameShapeColumn(base, shape.get(j))) { 1642 return true; 1643 } 1644 } 1645 } 1646 } 1647 return false; 1648 } 1649 1650 private boolean sameShapeColumn(String a, String b) { 1651 return gudusoft.gsqlparser.util.SQLUtil.sameName(vendor, 1652 gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, a, b); 1653 } 1654 1655 private static String syntheticDuplicateBase(String name) { 1656 if (name == null || !name.endsWith(")")) { // non-identifier-compare: generated display suffix punctuation 1657 return null; 1658 } 1659 int open = name.lastIndexOf('('); 1660 if (open <= 0 || open == name.length() - 2) { 1661 return null; 1662 } 1663 for (int i = open + 1; i < name.length() - 1; i++) { 1664 if (!Character.isDigit(name.charAt(i))) { 1665 return null; 1666 } 1667 } 1668 return name.substring(0, open); 1669 } 1670 1671 private static void addAll(List<Attributed> result, Set<Endpoint> endpoints, 1672 boolean fdr) { 1673 if (endpoints == null) { 1674 return; 1675 } 1676 for (Endpoint e : endpoints) { 1677 result.add(new Attributed(e, fdr)); 1678 } 1679 } 1680 1681 /** Declaration position of a callee formal endpoint, matched over the 1682 * identity's RAW declared spellings via the equality facade — 1683 * re-keying stored canonical text double-folds quoted names 1684 * (codex-B3-r2 finding 12). */ 1685 private int formalPositionOf(Endpoint formal, RoutineIdentity calleeId) { 1686 String name = formal.getColumn().length() > 0 1687 ? formal.getColumn() : formal.getParent(); 1688 // Endpoint parents can carry USE-qualification — match last segment. 1689 List<String> segments = gudusoft.gsqlparser.util.SQLUtil.parseNames(name); 1690 if (!segments.isEmpty()) { 1691 name = segments.get(segments.size() - 1); 1692 } 1693 if (name.startsWith("@")) { 1694 name = name.substring(1); 1695 } 1696 List<String> rawNames = calleeId.getRawParamNames(); 1697 for (int i = 0; i < rawNames.size(); i++) { 1698 String declared = rawNames.get(i); 1699 if (declared.startsWith("@")) { 1700 declared = declared.substring(1); 1701 } 1702 if (gudusoft.gsqlparser.util.SQLUtil.sameName(vendor, 1703 gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, 1704 name, declared)) { 1705 return i; 1706 } 1707 } 1708 return -1; 1709 } 1710}