001package gudusoft.gsqlparser.dlineage.dynamicsql; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TBaseType; 005import gudusoft.gsqlparser.TCustomSqlStatement; 006import gudusoft.gsqlparser.TSourceToken; 007import gudusoft.gsqlparser.TStatementList; 008import gudusoft.gsqlparser.dlineage.DataFlowAnalyzer; 009import gudusoft.gsqlparser.dlineage.dataflow.model.DynamicSqlSite; 010import gudusoft.gsqlparser.dlineage.dataflow.model.Option; 011import gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow; 012import gudusoft.gsqlparser.dlineage.dataflow.model.xml.relationship; 013import gudusoft.gsqlparser.dlineage.dataflow.model.xml.sourceColumn; 014import gudusoft.gsqlparser.dlineage.dataflow.model.xml.table; 015import gudusoft.gsqlparser.dlineage.dataflow.model.xml.targetColumn; 016import gudusoft.gsqlparser.nodes.TParameterDeclaration; 017import gudusoft.gsqlparser.nodes.TParameterDeclarationList; 018import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 019import gudusoft.gsqlparser.sqlenv.TSQLEnv; 020import gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure; 021import gudusoft.gsqlparser.util.SQLUtil; 022 023import java.io.UnsupportedEncodingException; 024import java.security.MessageDigest; 025import java.security.NoSuchAlgorithmException; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.TreeMap; 031 032/** 033 * Orchestrates dynamic-SQL lineage resolution: evaluate the procedure's 034 * dynamic-SQL string building under the given bindings 035 * ({@link TDynamicSqlStringEvaluator}), then for each materialized EXEC site run 036 * ordinary dlineage on the concrete string and tag the resulting edges with 037 * dynamic provenance. 038 * 039 * <p>This is purely additive — it constructs fresh, throwaway 040 * {@link DataFlowAnalyzer} instances for the materialized strings and never 041 * touches the default analysis path, so default dlineage output is unchanged. 042 */ 043public final class DynamicSqlLineageResolver { 044 045 private DynamicSqlLineageResolver() { 046 } 047 048 /** 049 * Materialized dynamic-SQL text for one EXEC / sp_executesql site, produced by the 050 * no-bindings default-path evaluation ({@link #materializeSitesWithoutBindings}). 051 */ 052 public static final class MaterializedSite { 053 /** Usable (possibly placeholder-bearing) SQL text, or {@code null} if the string did not reduce. */ 054 public final String sqlText; 055 /** True when the text still contains placeholder tokens for unbound variables/parameters. */ 056 public final boolean partial; 057 /** 058 * True unless the materialization is usable and every rendered character 059 * has literal provenance. This is intentionally stricter than 060 * {@link #partial}: a legacy-CONCRETE numeric/inexact transform can still 061 * have a provenance hole that SHADOW must report. 062 */ 063 public final boolean provenanceIncomplete; 064 /** Why the string did not reduce; non-null iff {@link #sqlText} is null. */ 065 public final String reason; 066 /** 067 * Ordered provenance fragments. When {@link #sqlText} is non-null their 068 * concatenated text equals it (design: 069 * dynamic-sql-fragment-provenance-design.md); SHADOW completeness checks 070 * and resolved dynamic edges consume that. When the string did not reduce 071 * ({@link #sqlText} null) they are the best-effort provenance of the 072 * argument — every hole met, in order, so a consumer can still name the 073 * value that stopped the fold; their concatenated text is only how far 074 * the fold got. Empty when the producing channel carries no provenance. 075 */ 076 public final java.util.List<SqlFragment> fragments; 077 /** Additive analysis note, such as a constant-relation over-approximation. */ 078 public final String diagnostic; 079 /** Stable value-set row-tuple suffix; empty on the scalar compatibility path. */ 080 public final String variantPath; 081 082 MaterializedSite(String sqlText, boolean partial, String reason) { 083 this(sqlText, partial, reason, null); 084 } 085 086 MaterializedSite(String sqlText, boolean partial, String reason, 087 java.util.List<SqlFragment> fragments) { 088 this(sqlText, partial, reason, fragments, null, ""); 089 } 090 091 MaterializedSite(String sqlText, boolean partial, String reason, 092 java.util.List<SqlFragment> fragments, String diagnostic, 093 String variantPath) { 094 this.sqlText = sqlText; 095 this.partial = partial; 096 this.reason = reason; 097 this.fragments = fragments == null || fragments.isEmpty() 098 ? java.util.Collections.<SqlFragment>emptyList() 099 : java.util.Collections.unmodifiableList( 100 new java.util.ArrayList<SqlFragment>(fragments)); 101 this.provenanceIncomplete = !DynamicSqlLineageResolver 102 .hasCompleteLiteralProvenance(sqlText, this.fragments); 103 this.diagnostic = diagnostic; 104 this.variantPath = variantPath == null ? "" : variantPath; 105 } 106 107 public boolean hasCompleteLiteralProvenance() { 108 return !provenanceIncomplete; 109 } 110 } 111 112 /** 113 * Default-path hook for {@link DataFlowAnalyzer}: abstractly evaluate a procedure's 114 * dynamic-SQL string building with NO call-site bindings — only literal parameter 115 * defaults are seeded — and return the materialized string per EXEC/sp_executesql 116 * statement node (identity-keyed against the SAME parse tree that was passed in). 117 * Constant- and template-built strings (REPLACE/QUOTENAME/CONCAT chains over 118 * literals and literal-seeded variables) reduce fully; strings depending on unbound 119 * parameters reduce to placeholder-bearing PARTIAL text; anything else is reported 120 * with a reason. Purely computational — nothing is executed and no model state of 121 * any analyzer is touched. 122 */ 123 public static Map<TCustomSqlStatement, MaterializedSite> materializeSitesWithoutBindings( 124 TCustomSqlStatement procAst, EDbVendor vendor, DynamicLineageOptions options) { 125 return materializeSites(procAst, vendor, null, options); 126 } 127 128 /** 129 * Same as {@link #materializeSitesWithoutBindings}, but with caller-supplied parameter 130 * bindings (e.g. extracted from a literal call site) seeded before the walk. Parameters 131 * absent from the bindings fall back to their literal defaults, then to placeholders. 132 */ 133 public static Map<TCustomSqlStatement, MaterializedSite> materializeSites( 134 TCustomSqlStatement procAst, EDbVendor vendor, Map<String, SqlValue> bindings, 135 DynamicLineageOptions options) { 136 List<Map<TCustomSqlStatement, MaterializedSite>> variants = 137 materializeSiteVariants(procAst, vendor, bindings, options); 138 return variants.isEmpty() 139 ? new java.util.IdentityHashMap<TCustomSqlStatement, MaterializedSite>() 140 : variants.get(0); 141 } 142 143 /** 144 * Materialize all bounded, row-correlated constant-relation variants for a 145 * procedure. Variant order is deterministic: declaration/statement order, 146 * then INSERT VALUES row order. The legacy single-map API above returns the 147 * first entry and is byte-compatible when the value-set feature is unused. 148 */ 149 public static List<Map<TCustomSqlStatement, MaterializedSite>> materializeSiteVariants( 150 TCustomSqlStatement procAst, EDbVendor vendor, Map<String, SqlValue> bindings, 151 DynamicLineageOptions options) { 152 if (options == null) { 153 options = DynamicLineageOptions.createDefault(); 154 } 155 List<Map<TCustomSqlStatement, MaterializedSite>> variants = 156 new ArrayList<Map<TCustomSqlStatement, MaterializedSite>>(); 157 // Same resource guardrails as resolve(): a binding count over the cap is 158 // rejected outright (best-effort empty result, since this hook must not throw 159 // into the default analysis path). 160 if (bindings != null && bindings.size() > options.getMaxBindings()) { 161 return variants; 162 } 163 TStatementList body = extractBody(procAst); 164 if (body == null) { 165 return variants; 166 } 167 long deadlineNanos = System.nanoTime() + options.getTimeoutMillis() * 1_000_000L; 168 List<ConstRelationVariantPlanner.EvaluationPlan> plans = 169 ConstRelationVariantPlanner.plan(body, vendor, options); 170 for (ConstRelationVariantPlanner.EvaluationPlan plan : plans) { 171 Map<TCustomSqlStatement, MaterializedSite> result = 172 new java.util.IdentityHashMap<TCustomSqlStatement, MaterializedSite>(); 173 TDynamicSqlStringEvaluator evaluator = 174 new TDynamicSqlStringEvaluator(vendor, options, plan, deadlineNanos); 175 evaluator.seedBindings(bindings); 176 evaluator.seedParameterDefaults(extractParams(procAst)); 177 for (TDynamicSqlStringEvaluator.RawSite raw : evaluator.evaluate(body)) { 178 if (raw.node == null) { 179 continue; 180 } 181 SqlStringValue sql = raw.sql; 182 if (sql != null && sql.usable() && sql.text != null 183 && sql.text.length() > options.getMaxResolvedSqlLength()) { 184 result.put(raw.node, new MaterializedSite(null, false, 185 "resolved SQL length " + sql.text.length() 186 + " exceeds maxResolvedSqlLength", 187 sql.fragments, raw.diagnostic, raw.variantPath)); 188 } else if (sql != null && sql.usable()) { 189 result.put(raw.node, new MaterializedSite(sql.text, 190 sql.state == SqlStringValue.State.PARTIAL, null, sql.fragments, 191 raw.diagnostic, raw.variantPath)); 192 } else { 193 // Unreduced: no text, but the value still carries the holes the 194 // evaluator met (which variable, assigned from what, where) — 195 // that is the site's unresolved-fragment evidence. 196 result.put(raw.node, new MaterializedSite(null, false, 197 sql != null && sql.reason != null ? sql.reason 198 : "dynamic SQL did not reduce to a concrete string", 199 sql == null ? null : sql.fragments, raw.diagnostic, raw.variantPath)); 200 } 201 } 202 variants.add(result); 203 } 204 return variants; 205 } 206 207 public static DynamicLineageResult resolve(TCustomSqlStatement procAst, EDbVendor vendor, TSQLEnv sqlEnv, 208 String currentDatabase, String defaultSchema, Map<String, SqlValue> bindings, 209 DynamicLineageOptions options) { 210 211 if (options == null) { 212 options = DynamicLineageOptions.createDefault(); 213 } 214 if (bindings != null && bindings.size() > options.getMaxBindings()) { 215 throw new IllegalArgumentException( 216 "binding count " + bindings.size() + " exceeds maxBindings " + options.getMaxBindings()); 217 } 218 219 TStatementList body = extractBody(procAst); 220 List<TParameterDeclaration> params = extractParams(procAst); 221 String sourceProc = extractProcName(procAst); 222 String bindingHash = hashBindings(bindings); 223 224 DynamicLineageResult result = new DynamicLineageResult(sourceProc, bindingHash); 225 if (body == null) { 226 return result; // not a recognized procedure shape — nothing to evaluate 227 } 228 229 long deadlineNanos = System.nanoTime() + options.getTimeoutMillis() * 1_000_000L; 230 List<ConstRelationVariantPlanner.EvaluationPlan> plans = 231 ConstRelationVariantPlanner.plan(body, vendor, options); 232 java.util.IdentityHashMap<TCustomSqlStatement, java.util.Set<String>> seen = 233 new java.util.IdentityHashMap<TCustomSqlStatement, java.util.Set<String>>(); 234 boolean truncated = false; 235 int emitted = 0; 236 for (ConstRelationVariantPlanner.EvaluationPlan plan : plans) { 237 TDynamicSqlStringEvaluator evaluator = 238 new TDynamicSqlStringEvaluator(vendor, options, plan, deadlineNanos); 239 evaluator.seedBindings(bindings); 240 evaluator.seedParameterDefaults(params); 241 for (TDynamicSqlStringEvaluator.RawSite raw : evaluator.evaluate(body)) { 242 if (raw.node == null) { 243 continue; 244 } 245 String valueKey = raw.sql == null ? "<null>" 246 : raw.sql.state + "\u0000" + raw.sql.text + "\u0000" + raw.sql.reason 247 + "\u0000" + raw.controlFlowCertain; 248 java.util.Set<String> values = seen.get(raw.node); 249 if (values == null) { 250 values = new java.util.LinkedHashSet<String>(); 251 seen.put(raw.node, values); 252 } 253 if (!values.add(valueKey)) { 254 continue; 255 } 256 if (emitted >= options.getMaxSites()) { 257 truncated = true; 258 break; 259 } 260 result.addSite(buildSite(raw, vendor, sqlEnv, currentDatabase, 261 defaultSchema, sourceProc, bindingHash, options)); 262 emitted++; 263 } 264 truncated |= evaluator.isSitesTruncated(); 265 if (truncated) { 266 break; 267 } 268 } 269 // Never let a truncated walk look complete — emit an honest diagnostic. 270 if (truncated) { 271 result.addSite(new DynamicSiteResult(DynamicSiteResult.Kind.EXEC_STRING, 272 DynamicSiteResult.Status.UNRESOLVED, null, null, sourceProc, 273 dynamicSiteId(sourceProc, "truncated"), bindingHash, 274 "site/time limit reached (maxSites=" + options.getMaxSites() 275 + ", timeout=" + options.getTimeoutMillis() + "ms); further sites dropped", 276 0L, 0L, new ArrayList<DynamicLineageEdge>(), options.getTrustMode(), 277 0, -1, false)); 278 } 279 return result; 280 } 281 282 /** 283 * Discover parameter bindings from literal call sites of {@code procAst} in the same 284 * parsed file: top-level {@code EXEC <proc> 'literal', @p = 'literal', ...} statements 285 * whose (unqualified) module name matches the procedure name. Positional and named 286 * arguments are supported; OUTPUT and non-literal arguments are left unbound (they 287 * become placeholders during evaluation). Returns up to {@code maxBindingSets} distinct 288 * binding sets in file order. 289 */ 290 public static List<Map<String, SqlValue>> discoverCallSiteBindings(TCustomSqlStatement procAst, 291 int maxBindingSets) { 292 List<Map<String, SqlValue>> bindingSets = new ArrayList<Map<String, SqlValue>>(); 293 if (!(procAst instanceof TMssqlCreateProcedure) || procAst.getGsqlparser() == null) { 294 return bindingSets; 295 } 296 TMssqlCreateProcedure proc = (TMssqlCreateProcedure) procAst; 297 if (proc.getProcedureName() == null) { 298 return bindingSets; 299 } 300 String procSimpleName = lastNameSegment(proc.getProcedureName().toString()); 301 List<TParameterDeclaration> params = extractParams(procAst); 302 String procDb = effectiveDatabaseOfProc(procAst.getGsqlparser().getSqlstatements(), procAst); 303 for (BindingCandidate candidate : sameFileBindingCandidates(procAst, proc, procSimpleName, 304 params, procDb, maxBindingSets)) { 305 bindingSets.add(candidate.bindings); 306 } 307 return bindingSets; 308 } 309 310 /** 311 * Matching call sites in the procedure's own file, in file order, deduplicated by 312 * canonical key, up to {@code cap}. Tracks the effective database (USE statements) 313 * so a call after USE DbB is never bound to a same-named procedure defined under 314 * USE DbA. 315 */ 316 private static List<BindingCandidate> sameFileBindingCandidates(TCustomSqlStatement procAst, 317 TMssqlCreateProcedure proc, String procSimpleName, List<TParameterDeclaration> params, 318 String procDb, int cap) { 319 List<BindingCandidate> result = new ArrayList<BindingCandidate>(); 320 java.util.Set<String> seen = new java.util.HashSet<String>(); 321 TStatementList all = procAst.getGsqlparser().getSqlstatements(); 322 String currentDb = null; 323 for (int i = 0; i < all.size() && result.size() < cap; i++) { 324 if (all.get(i) instanceof gudusoft.gsqlparser.stmt.TUseDatabase) { 325 currentDb = String.valueOf(((gudusoft.gsqlparser.stmt.TUseDatabase) all.get(i)).getDatabaseName()); 326 continue; 327 } 328 if (!(all.get(i) instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlExecute)) { 329 continue; 330 } 331 gudusoft.gsqlparser.stmt.mssql.TMssqlExecute call = 332 (gudusoft.gsqlparser.stmt.mssql.TMssqlExecute) all.get(i); 333 if (!callMatchesProc(call, proc, procSimpleName, procDb, currentDb)) { 334 continue; 335 } 336 BindingCandidate candidate = bindingCandidateFromCall(call, params); 337 if (candidate != null && seen.add(candidate.canonicalKey)) { 338 result.add(candidate); 339 } 340 } 341 return result; 342 } 343 344 /** 345 * Discover parameter bindings from literal call sites of {@code procAst} located in 346 * OTHER files of the same multi-file analysis (plan Phase 5: cross-file routine 347 * references). {@code otherFileSqlTexts} holds the raw SQL text of every analysis 348 * unit; the procedure's own file is skipped by text identity. A text is parsed at 349 * most once per analysis via the caller-owned {@code parseCache} (key: the text), 350 * and only when it mentions the procedure's simple name at all. 351 * 352 * <p>Never guesses across ambiguity: if any other file defines a procedure with the 353 * same simple name in a database-compatible context, there are two candidate owners 354 * for a matching call and this method returns no bindings at all. Binding sets are 355 * returned in canonical-key order, so the result is independent of the file order 356 * of the analysis. Binding sets already surfaced by the same-file discovery above 357 * are excluded here, so duplicates never consume the caller's shared cap. 358 */ 359 public static List<Map<String, SqlValue>> discoverCrossFileCallSiteBindings( 360 TCustomSqlStatement procAst, RoutineCatalog catalog, int maxBindingSets) { 361 List<Map<String, SqlValue>> bindingSets = new ArrayList<Map<String, SqlValue>>(); 362 if (maxBindingSets <= 0 || catalog == null 363 || !(procAst instanceof TMssqlCreateProcedure) 364 || procAst.getGsqlparser() == null) { 365 return bindingSets; 366 } 367 TMssqlCreateProcedure proc = (TMssqlCreateProcedure) procAst; 368 if (proc.getProcedureName() == null) { 369 return bindingSets; 370 } 371 String procSimpleName = lastNameSegment(proc.getProcedureName().toString()); 372 String probeName = bareName(procSimpleName); 373 if (probeName.isEmpty()) { 374 return bindingSets; 375 } 376 List<TParameterDeclaration> params = extractParams(procAst); 377 String ownText = procAst.getGsqlparser().sqltext; 378 // For matching and the ambiguity verdict, an explicit catalog on the target 379 // definition's own three-part name outranks the USE-derived database (a 380 // definition CREATE PROC DbA.dbo.p IS provably in DbA). 381 String sameFileProcDb = effectiveDatabaseOfProc(procAst.getGsqlparser().getSqlstatements(), procAst); 382 String procDb = definitionDatabase(proc, sameFileProcDb); 383 384 // The same-file pass already surfaced these binding sets (cross-file is only 385 // invoked when its cap was not reached, so an uncapped re-collection sees 386 // exactly the sets it returned). Excluding their keys keeps duplicates from 387 // consuming the shared cap and displacing distinct cross-file bindings. 388 java.util.Set<String> sameFileKeys = new java.util.HashSet<String>(); 389 for (BindingCandidate sameFile : sameFileBindingCandidates(procAst, proc, procSimpleName, 390 params, sameFileProcDb, Integer.MAX_VALUE)) { 391 sameFileKeys.add(sameFile.canonicalKey); 392 } 393 394 // Canonical-key ordering makes the accepted binding sets (and which ones 395 // survive the cap) independent of the order files were passed in. 396 java.util.TreeMap<String, Map<String, SqlValue>> ordered = 397 new java.util.TreeMap<String, Map<String, SqlValue>>(); 398 for (String text : catalog.unitTexts()) { 399 if (text == null || text.equals(ownText)) { 400 continue; 401 } 402 if (!text.toLowerCase(java.util.Locale.ENGLISH) 403 .contains(probeName.toLowerCase(java.util.Locale.ENGLISH))) { 404 continue; 405 } 406 RoutineCatalog.UnitIndex unit = catalog.indexOf(text); 407 if (unit == null) { 408 continue; 409 } 410 for (RoutineCatalog.Definition def : unit.definitions) { 411 // A same-named definition in another file: unless the two are 412 // provably in different databases, a matching call has two 413 // candidate owners - bail out entirely rather than guess. 414 if (SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotProcedure, 415 def.simple, procSimpleName) 416 && !provablyDifferentDatabase(procDb, def.db)) { 417 return new ArrayList<Map<String, SqlValue>>(); 418 } 419 } 420 for (RoutineCatalog.CallCandidate callCandidate : unit.callCandidates) { 421 if (!(callCandidate.stmt instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlExecute)) { 422 continue; 423 } 424 gudusoft.gsqlparser.stmt.mssql.TMssqlExecute call = 425 (gudusoft.gsqlparser.stmt.mssql.TMssqlExecute) callCandidate.stmt; 426 if (!callMatchesProc(call, proc, procSimpleName, procDb, callCandidate.db)) { 427 continue; 428 } 429 BindingCandidate candidate = bindingCandidateFromCall(call, params); 430 if (candidate != null && !sameFileKeys.contains(candidate.canonicalKey) 431 && !ordered.containsKey(candidate.canonicalKey)) { 432 ordered.put(candidate.canonicalKey, candidate.bindings); 433 } 434 } 435 } 436 for (Map<String, SqlValue> bindings : ordered.values()) { 437 if (bindingSets.size() >= maxBindingSets) { 438 break; 439 } 440 bindingSets.add(bindings); 441 } 442 return bindingSets; 443 } 444 445 /** 446 * Oracle analog of cross-file call-site discovery (plan Phase 5): collect 447 * top-level statements from OTHER files (anonymous blocks, CALL statements) 448 * that mention a routine defined in the current file, so the caller can feed 449 * them to {@code TASTEvaluator} together with the file's own statements and 450 * fold parameter-dependent EXECUTE IMMEDIATE templates under the callers' 451 * literal arguments. 452 * 453 * <p>Mirrors the MSSQL rules: a text is parsed at most once per analysis via 454 * {@code parseCache} and only when it mentions one of the candidate routine 455 * names; a same-simple-name routine DEFINITION in another file makes that 456 * name ambiguous and drops it from the harvest (never guess between two 457 * candidate owners); foreign definitions themselves are never appended. 458 * Returned statements are ordered by their source text, so evaluation order 459 * is independent of the file order of the analysis. 460 */ 461 public static List<TCustomSqlStatement> plsqlInterpretationStatements( 462 TStatementList ownStatements, String ownText, RoutineCatalog catalog, 463 int maxForeignCallers) { 464 List<TCustomSqlStatement> result = new ArrayList<TCustomSqlStatement>(); 465 if (ownStatements == null) { 466 return result; 467 } 468 // Routine definitions in this file - the possible callees. Only routines whose 469 // partial dynamic-SQL expressions are provably literal-bindable participate: 470 // the interpreter folds any dependency it cannot evaluate (SELECT INTO 471 // locals, function results, ...) to an EMPTY string, which would produce a 472 // concrete-looking but WRONG statement that the placeholder classifier 473 // cannot catch. 474 List<PlsqlRoutineIdent> idents = new ArrayList<PlsqlRoutineIdent>(); 475 for (int i = 0; i < ownStatements.size(); i++) { 476 TCustomSqlStatement stmt = ownStatements.get(i); 477 String name = plsqlRoutineSimpleName(stmt); 478 if (name == null || name.isEmpty()) { 479 continue; 480 } 481 if (plsqlDynamicSqlLiteralBindable(stmt)) { 482 idents.add(new PlsqlRoutineIdent(stmt, name)); 483 } 484 } 485 if (idents.isEmpty()) { 486 return result; 487 } 488 // Foreign pass 1: definitions in other files decide ambiguity. A same-simple- 489 // name definition that is not provably in a different schema means a matching 490 // call has two candidate owners. 491 List<RoutineCatalog.UnitIndex> foreignUnits = new ArrayList<RoutineCatalog.UnitIndex>(); 492 if (catalog != null) { 493 for (String text : catalog.unitTexts()) { 494 if (text == null || text.equals(ownText)) { 495 continue; 496 } 497 String lowered = text.toLowerCase(java.util.Locale.ENGLISH); 498 boolean mentionsAny = false; 499 for (PlsqlRoutineIdent ident : idents) { 500 if (lowered.contains(bareName(ident.simple).toLowerCase(java.util.Locale.ENGLISH))) { 501 mentionsAny = true; 502 break; 503 } 504 } 505 if (!mentionsAny) { 506 continue; 507 } 508 RoutineCatalog.UnitIndex unit = catalog.indexOf(text); 509 if (unit == null) { 510 continue; 511 } 512 foreignUnits.add(unit); 513 for (RoutineCatalog.Definition def : unit.definitions) { 514 for (PlsqlRoutineIdent ident : idents) { 515 if (!SQLUtil.sameName(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 516 def.simple, ident.simple)) { 517 continue; 518 } 519 // ANY same-simple-name duplicate disqualifies UNQUALIFIED calls: 520 // the caller's current schema is unknown, so even a provably 521 // different schema could be the one the call resolves in. 522 ident.hasForeignSameName = true; 523 if (!provablyDifferentSchema(ident.schema, def.schema)) { 524 // Not provably different: qualified calls cannot separate 525 // the two candidates either. 526 ident.ambiguous = true; 527 } 528 } 529 } 530 } 531 } 532 // Same-file callers (file order, no cap - they were always visible to the 533 // analysis), then foreign callers ordered by their EXACT source text so the 534 // pick under the cap is independent of file order and distinct quoted 535 // literals are never conflated. 536 List<TCustomSqlStatement> callers = new ArrayList<TCustomSqlStatement>(); 537 for (int i = 0; i < ownStatements.size(); i++) { 538 TCustomSqlStatement stmt = ownStatements.get(i); 539 if (plsqlRoutineSimpleName(stmt) != null || isPlsqlDefinitionShape(stmt)) { 540 continue; 541 } 542 if (isVettedLiteralCaller(stmt, idents)) { 543 callers.add(stmt); 544 } 545 } 546 java.util.TreeMap<String, TCustomSqlStatement> ordered = 547 new java.util.TreeMap<String, TCustomSqlStatement>(); 548 for (RoutineCatalog.UnitIndex unit : foreignUnits) { 549 for (RoutineCatalog.CallCandidate callCandidate : unit.callCandidates) { 550 TCustomSqlStatement stmt = callCandidate.stmt; 551 if (isPlsqlDefinitionShape(stmt)) { 552 continue; 553 } 554 if (isVettedLiteralCaller(stmt, idents)) { 555 String key = String.valueOf(stmt.toString()); 556 if (!ordered.containsKey(key)) { 557 ordered.put(key, stmt); 558 } 559 } 560 } 561 } 562 int foreignAdded = 0; 563 List<TCustomSqlStatement> foreignCallers = new ArrayList<TCustomSqlStatement>(); 564 for (TCustomSqlStatement stmt : ordered.values()) { 565 if (foreignAdded >= maxForeignCallers) { 566 break; 567 } 568 foreignCallers.add(stmt); 569 foreignAdded++; 570 } 571 callers.addAll(foreignCallers); 572 if (callers.isEmpty()) { 573 // No vetted caller anywhere - interpretation would bind nothing. 574 return result; 575 } 576 for (PlsqlRoutineIdent ident : idents) { 577 result.add(ident.definition); 578 } 579 result.addAll(callers); 580 return result; 581 } 582 583 /** Qualified identity of an own-file routine definition, plus its ambiguity verdicts. */ 584 private static final class PlsqlRoutineIdent { 585 final TCustomSqlStatement definition; 586 final String schema; // null when the definition name is unqualified 587 final String simple; 588 /** Some other file defines the same simple name AND it is not provably in a different schema. */ 589 boolean ambiguous; 590 /** Some other file defines the same simple name at all (disqualifies unqualified calls). */ 591 boolean hasForeignSameName; 592 593 PlsqlRoutineIdent(TCustomSqlStatement definition, String simple) { 594 this.definition = definition; 595 this.schema = plsqlRoutineSchema(definition); 596 this.simple = simple; 597 } 598 } 599 600 /** Schema segment of a routine definition name; null when unqualified. */ 601 private static String plsqlRoutineSchema(TCustomSqlStatement stmt) { 602 gudusoft.gsqlparser.nodes.TObjectName name = null; 603 if (stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) { 604 name = ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) stmt).getProcedureName(); 605 } else if (stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) { 606 name = ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) stmt).getFunctionName(); 607 } 608 if (name == null) { 609 return null; 610 } 611 List<String> parts = SQLUtil.parseNames(name.toString()); 612 return parts.size() >= 2 ? parts.get(parts.size() - 2) : null; 613 } 614 615 /** 616 * True only when the two schema contexts are BOTH known and differ; a null on 617 * either side could be the same schema (the conservative direction for the 618 * ambiguity verdict). 619 */ 620 private static boolean provablyDifferentSchema(String schemaA, String schemaB) { 621 if (schemaA == null || schemaB == null) { 622 return false; 623 } 624 return !SQLUtil.sameName(EDbVendor.dbvoracle, ESQLDataObjectType.dotSchema, schemaA, schemaB); 625 } 626 627 /** 628 * Definition shapes that must never enter the interpretation list. Anonymous 629 * blocks (TCommonBlock) ARE TStoredProcedureSqlStatement subclasses and are the 630 * primary caller shape, so this names the definition classes explicitly. 631 */ 632 private static boolean isPlsqlDefinitionShape(TCustomSqlStatement stmt) { 633 return stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage 634 || stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateTrigger 635 || stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateType; 636 } 637 638 /** 639 * True when {@code stmt} contains at least one call that provably targets one of 640 * {@code idents} and EVERY name-colliding call in the statement (a) resolves to 641 * exactly one non-ambiguous identity - unqualified calls match by simple name, 642 * two-part calls require the definition's schema to match, anything the shape 643 * rules cannot prove is rejected because the interpreter may still bind it by 644 * simple name - and (b) passes only compile-time literal arguments. A call fed 645 * by a variable or expression is rejected wholesale: the interpreter would fold 646 * the unevaluated operand to an EMPTY string - not a placeholder - which can 647 * silently produce wrong object names (e.g. 'x' || p_suffix folding to just 648 * "x"). The mssql discovery applies the same literal-only discipline. 649 */ 650 private static boolean isVettedLiteralCaller(TCustomSqlStatement stmt, 651 List<PlsqlRoutineIdent> idents) { 652 final boolean[] found = { false }; 653 final boolean[] rejected = { false }; 654 final List<PlsqlRoutineIdent> targets = idents; 655 try { 656 stmt.acceptChildren(new gudusoft.gsqlparser.nodes.TParseTreeVisitor() { 657 public void preVisit(gudusoft.gsqlparser.nodes.TFunctionCall call) { 658 vetCall(call); 659 } 660 661 public void preVisit(gudusoft.gsqlparser.stmt.TCallStatement call) { 662 // TCallStatement.acceptChildren does not surface its routine 663 // expression as a TFunctionCall - vet it explicitly, else 664 // CALL p('src') callers are silently never harvested. 665 if (call.getRoutineExpr() != null) { 666 vetCall(call.getRoutineExpr().getFunctionCall()); 667 } 668 } 669 670 private void vetCall(gudusoft.gsqlparser.nodes.TFunctionCall call) { 671 if (call == null || call.getFunctionName() == null) { 672 return; 673 } 674 List<String> parts = SQLUtil.parseNames(call.getFunctionName().toString()); 675 if (parts.isEmpty()) { 676 return; 677 } 678 String simple = parts.get(parts.size() - 1); 679 PlsqlRoutineIdent matched = null; 680 boolean collides = false; 681 boolean multi = false; 682 for (PlsqlRoutineIdent ident : targets) { 683 if (!SQLUtil.sameName(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 684 simple, ident.simple)) { 685 continue; 686 } 687 collides = true; 688 boolean resolves; 689 if (parts.size() == 1) { 690 // An unqualified call resolves in the CALLER's (unknown) 691 // current schema: any same-named definition elsewhere - 692 // even in a provably different schema - leaves two 693 // candidates. 694 resolves = !ident.hasForeignSameName && !ident.ambiguous; 695 } else if (parts.size() == 2) { 696 resolves = !ident.ambiguous && ident.schema != null 697 && SQLUtil.sameName(EDbVendor.dbvoracle, ESQLDataObjectType.dotSchema, 698 parts.get(0), ident.schema); 699 } else { 700 resolves = false; 701 } 702 if (resolves) { 703 if (matched != null) { 704 multi = true; 705 } 706 matched = ident; 707 } 708 } 709 if (!collides) { 710 return; 711 } 712 if (matched == null || multi) { 713 rejected[0] = true; 714 return; 715 } 716 if (call.getArgs() == null || call.getArgs().size() == 0) { 717 found[0] = true; 718 return; 719 } 720 for (int i = 0; i < call.getArgs().size(); i++) { 721 if (!isCompileTimeLiteral(call.getArgs().getExpression(i))) { 722 rejected[0] = true; 723 return; 724 } 725 } 726 found[0] = true; 727 } 728 }); 729 } catch (RuntimeException ex) { 730 return false; 731 } 732 return found[0] && !rejected[0]; 733 } 734 735 /** 736 * F1 guard: true when every partial/unfolded EXECUTE IMMEDIATE inside 737 * {@code routine} builds its SQL exclusively from compile-time literals, 738 * routine parameters, and local variables whose every assignment (and declared 739 * default) is itself so composed, connected by || concatenation. Any other 740 * dependency - SELECT/FETCH/EXECUTE..INTO targets, function results, variables 741 * passed to other calls (possible OUT), nested routine declarations (scope 742 * conflation) - disqualifies the whole routine: the interpreter folds what it 743 * cannot evaluate to an EMPTY string, which would fabricate concrete-looking 744 * but wrong SQL that the placeholder classifier cannot detect. 745 */ 746 private static boolean plsqlDynamicSqlLiteralBindable(TCustomSqlStatement routine) { 747 PlsqlBindContext ctx = buildPlsqlBindContext(routine); 748 if (ctx == null || ctx.unbindable) { 749 return false; 750 } 751 for (gudusoft.gsqlparser.nodes.TExpression expr : ctx.partialDynamicExprs) { 752 if (!isLiteralBindableExpr(expr, ctx.params, ctx.assignments, ctx.disallowed, 753 new java.util.HashSet<String>())) { 754 return false; 755 } 756 } 757 return true; 758 } 759 760 /** 761 * Binding-relevant facts collected from one routine's body, shared between the 762 * EXECUTE IMMEDIATE bindability gate and the DBMS_SQL text fold: declared 763 * parameters, tracked single-variable assignments, names disqualified by 764 * untrackable writes, and the dynamic-string expressions of partial/unfolded 765 * EXECUTE IMMEDIATE sites. 766 */ 767 static final class PlsqlBindContext { 768 final java.util.Set<String> params = new java.util.HashSet<String>(); 769 final Map<String, List<BoundSource>> assignments = 770 new HashMap<String, List<BoundSource>>(); 771 final java.util.Set<String> disallowed = new java.util.HashSet<String>(); 772 final List<gudusoft.gsqlparser.nodes.TExpression> partialDynamicExprs = 773 new ArrayList<gudusoft.gsqlparser.nodes.TExpression>(); 774 /** Nested routine declarations (scope conflation) or a partial site with no expression. */ 775 boolean unbindable; 776 } 777 778 /** One tracked assignment source with its source position (for flow-order checks). */ 779 static final class BoundSource { 780 final gudusoft.gsqlparser.nodes.TExpression expr; 781 final long line; 782 final long col; 783 784 BoundSource(gudusoft.gsqlparser.nodes.TExpression expr, long line, long col) { 785 this.expr = expr; 786 this.line = line; 787 this.col = col; 788 } 789 } 790 791 /** Collector behind {@link PlsqlBindContext}; null when the walk crashed. */ 792 static PlsqlBindContext buildPlsqlBindContext(TCustomSqlStatement routine) { 793 try { 794 final PlsqlBindContext ctx = new PlsqlBindContext(); 795 final java.util.Set<String> params = ctx.params; 796 if (routine instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) { 797 TParameterDeclarationList declared = ((gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) routine) 798 .getParameterDeclarations(); 799 if (declared != null) { 800 for (int i = 0; i < declared.size(); i++) { 801 TParameterDeclaration pd = declared.getParameterDeclarationItem(i); 802 if (pd != null && pd.getParameterName() != null) { 803 params.add(canonVarKey(pd.getParameterName().toString())); 804 } 805 } 806 } 807 } 808 final Map<String, List<BoundSource>> assignments = ctx.assignments; 809 final java.util.Set<String> disallowed = ctx.disallowed; 810 final List<gudusoft.gsqlparser.nodes.TExpression> dynamicExprs = ctx.partialDynamicExprs; 811 final boolean[] nestedRoutine = { false }; 812 final TCustomSqlStatement root = routine; 813 routine.acceptChildren(new gudusoft.gsqlparser.nodes.TParseTreeVisitor() { 814 public void preVisit(gudusoft.gsqlparser.stmt.TAssignStmt stmt) { 815 if (stmt.getLeft() == null || stmt.getExpression() == null) { 816 return; 817 } 818 String left = stmt.getLeft().toString(); 819 if (left == null || left.indexOf('(') >= 0 820 || SQLUtil.parseNames(left).size() != 1) { 821 return; // record fields / collection elements are not tracked 822 } 823 recordAssignment(canonVarKey(left), stmt.getExpression(), 824 stmt.getStartToken() != null ? stmt.getStartToken() 825 : stmt.getExpression().getStartToken()); 826 } 827 828 public void preVisit(gudusoft.gsqlparser.stmt.TVarDeclStmt stmt) { 829 if (stmt.getElementName() == null) { 830 return; 831 } 832 if (stmt.getDefaultValue() != null) { 833 recordAssignment(canonVarKey(stmt.getElementName().toString()), 834 stmt.getDefaultValue(), stmt.getStartToken()); 835 } 836 } 837 838 private void recordAssignment(String key, 839 gudusoft.gsqlparser.nodes.TExpression source, 840 gudusoft.gsqlparser.TSourceToken position) { 841 List<BoundSource> list = assignments.get(key); 842 if (list == null) { 843 list = new ArrayList<BoundSource>(); 844 assignments.put(key, list); 845 } 846 // Unknown position sorts AFTER everything (Long.MAX_VALUE): a source 847 // whose location cannot be established never counts as preceding a 848 // use site - the conservative direction for the flow-order check. 849 list.add(new BoundSource(source, 850 position != null ? position.lineNo : Long.MAX_VALUE, 851 position != null ? position.columnNo : Long.MAX_VALUE)); 852 } 853 854 public void preVisit(gudusoft.gsqlparser.stmt.TSelectSqlStatement stmt) { 855 if (stmt.getIntoClause() != null && stmt.getIntoClause().getExprList() != null) { 856 for (int i = 0; i < stmt.getIntoClause().getExprList().size(); i++) { 857 disallowExpr(stmt.getIntoClause().getExprList().getExpression(i)); 858 } 859 } 860 } 861 862 public void preVisit(gudusoft.gsqlparser.stmt.TFetchStmt stmt) { 863 if (stmt.getVariableNames() != null) { 864 for (int i = 0; i < stmt.getVariableNames().size(); i++) { 865 disallowExpr(stmt.getVariableNames().getExpression(i)); 866 } 867 } 868 } 869 870 public void preVisit(gudusoft.gsqlparser.stmt.TExecImmeStmt stmt) { 871 if (stmt.getIntoVariables() != null) { 872 for (int i = 0; i < stmt.getIntoVariables().size(); i++) { 873 disallowExpr(stmt.getIntoVariables().getExpression(i)); 874 } 875 } 876 // USING bind values never rewrite the SQL text, but OUT binds 877 // write variables - disallow every bind target conservatively. 878 if (stmt.getBindArguments() != null) { 879 for (int i = 0; i < stmt.getBindArguments().size(); i++) { 880 gudusoft.gsqlparser.nodes.TBindArgument bind = 881 stmt.getBindArguments().getBindArgument(i); 882 if (bind != null && bind.getBindArgumentExpr() != null) { 883 disallowExpr(bind.getBindArgumentExpr()); 884 } 885 } 886 } 887 if (stmt.getDynamicSQL() == null || stmt.isDynamicSQLPartial()) { 888 if (stmt.getDynamicStringExpr() != null) { 889 dynamicExprs.add(stmt.getDynamicStringExpr()); 890 } else { 891 nestedRoutine[0] = true; // unfoldable site with no expression - bail 892 } 893 } 894 } 895 896 public void preVisit(gudusoft.gsqlparser.nodes.TFunctionCall call) { 897 disallowBareArgs(call); 898 } 899 900 public void preVisit(gudusoft.gsqlparser.stmt.TCallStatement stmt) { 901 // TCallStatement.acceptChildren does not surface its routine 902 // expression as a TFunctionCall - inspect its args explicitly. 903 if (stmt.getRoutineExpr() != null) { 904 disallowBareArgs(stmt.getRoutineExpr().getFunctionCall()); 905 } 906 } 907 908 public void preVisit(gudusoft.gsqlparser.nodes.TReturningClause clause) { 909 // RETURNING ... INTO writes locals, and the clause's own 910 // acceptChildren does not visit its variable list - handle it 911 // here or a literal-initialized local silently keeps its stale 912 // literal after being overwritten. 913 if (clause.getVariableList() != null) { 914 for (int i = 0; i < clause.getVariableList().size(); i++) { 915 disallowExpr(clause.getVariableList().getExpression(i)); 916 } 917 } 918 } 919 920 private void disallowBareArgs(gudusoft.gsqlparser.nodes.TFunctionCall call) { 921 // A bare variable handed to any call could be an OUT actual - 922 // except at argument positions of KNOWN in-only builtins 923 // (notably DBMS_SQL.PARSE's own statement argument, which 924 // would otherwise disqualify the very variable it consumes). 925 if (call == null || call.getArgs() == null) { 926 return; 927 } 928 for (int i = 0; i < call.getArgs().size(); i++) { 929 if (isKnownInOnlyArg(call, i)) { 930 continue; 931 } 932 gudusoft.gsqlparser.nodes.TExpression arg = call.getArgs().getExpression(i); 933 if (arg != null && arg.getExpressionType() 934 == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 935 disallowExpr(arg); 936 } 937 } 938 } 939 940 public void preVisit(gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure stmt) { 941 if (stmt != root) { 942 nestedRoutine[0] = true; // nested scopes would conflate name keys 943 } 944 } 945 946 public void preVisit(gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction stmt) { 947 if (stmt != root) { 948 nestedRoutine[0] = true; 949 } 950 } 951 952 private void disallowExpr(gudusoft.gsqlparser.nodes.TExpression expr) { 953 if (expr == null) { 954 return; 955 } 956 String text = expr.toString(); 957 if (text != null && SQLUtil.parseNames(text).size() == 1 958 && text.indexOf('(') < 0) { 959 disallowed.add(canonVarKey(text)); 960 } 961 } 962 }); 963 ctx.unbindable = nestedRoutine[0]; 964 return ctx; 965 } catch (RuntimeException ex) { 966 return null; 967 } 968 } 969 970 /** 971 * True when argument {@code index} of {@code call} is an IN-only position of a 972 * known builtin, so a bare variable there is a READ and must not disqualify the 973 * variable from literal folding. Deliberately tiny: DBMS_SQL.PARSE's statement 974 * argument (index 1 positional; named association arrives as an arrow 975 * expression and is never a bare name anyway) and DBMS_OUTPUT.PUT_LINE / PUT 976 * (pure logging, all-IN) — the calls that most commonly surround dynamic SQL. 977 */ 978 private static boolean isKnownInOnlyArg(gudusoft.gsqlparser.nodes.TFunctionCall call, int index) { 979 if (call.getFunctionName() == null) { 980 return false; 981 } 982 String name = call.getFunctionName().toString(); 983 if (SQLUtil.compareIdentifier(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 984 name, "DBMS_SQL.PARSE") 985 || SQLUtil.compareIdentifier(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 986 name, "SYS.DBMS_SQL.PARSE")) { 987 return index == 1; 988 } 989 return SQLUtil.compareIdentifier(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 990 name, "DBMS_OUTPUT.PUT_LINE") 991 || SQLUtil.compareIdentifier(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 992 name, "DBMS_OUTPUT.PUT"); 993 } 994 995 /** 996 * DBMS_SQL entry point (plan Phase 5 slice 4): fold the SQL-text argument of a 997 * {@code DBMS_SQL.PARSE} call into concrete statement texts. Returns the 998 * no-binding fold first (literal or literal-composed locals), then one fold per 999 * literal call-site binding set (same-file callers first, then cross-file via 1000 * the catalog), deduplicated, capped at {@code maxVariants}. Empty when nothing 1001 * folds — the site then keeps its honest unresolved diagnostic. The fold shares 1002 * the EXECUTE IMMEDIATE whitelist (literals, parameters bound at vetted call 1003 * sites, locals with exactly one literal-composed assignment, || and 1004 * parentheses), so a fold can never fabricate object names. 1005 */ 1006 public static List<String> foldDbmsSqlTexts(TCustomSqlStatement enclosingRoutine, 1007 gudusoft.gsqlparser.nodes.TExpression sqlArg, RoutineCatalog catalog, int maxVariants) { 1008 List<String> texts = new ArrayList<String>(); 1009 if (sqlArg == null || maxVariants <= 0) { 1010 return texts; 1011 } 1012 PlsqlBindContext ctx = enclosingRoutine == null ? new PlsqlBindContext() 1013 : buildPlsqlBindContext(enclosingRoutine); 1014 if (ctx == null || ctx.unbindable) { 1015 // Context unavailable: only a directly-literal argument can fold. 1016 ctx = new PlsqlBindContext(); 1017 } 1018 // Flow-order bound: only assignments strictly before the PARSE site count. 1019 // An unknown site position folds no variables at all (literals still do). 1020 long siteLine = Long.MIN_VALUE; 1021 long siteCol = Long.MIN_VALUE; 1022 if (sqlArg.getStartToken() != null) { 1023 siteLine = sqlArg.getStartToken().lineNo; 1024 siteCol = sqlArg.getStartToken().columnNo; 1025 } 1026 String direct = foldLiteralBindableExpr(sqlArg, ctx, 1027 new HashMap<String, SqlValue>(), new java.util.HashSet<String>(), 1028 siteLine, siteCol); 1029 if (direct != null && !direct.trim().isEmpty()) { 1030 texts.add(direct); 1031 } 1032 if (texts.size() < maxVariants && enclosingRoutine != null) { 1033 for (Map<String, SqlValue> bindings : discoverPlsqlCallSiteBindings( 1034 enclosingRoutine, catalog, maxVariants)) { 1035 String folded = foldLiteralBindableExpr(sqlArg, ctx, bindings, 1036 new java.util.HashSet<String>(), siteLine, siteCol); 1037 if (folded != null && !folded.trim().isEmpty() && !texts.contains(folded)) { 1038 texts.add(folded); 1039 if (texts.size() >= maxVariants) { 1040 break; 1041 } 1042 } 1043 } 1044 } 1045 return texts; 1046 } 1047 1048 /** 1049 * Fold an expression to its concrete string under the P5-2 whitelist: literals, 1050 * parameters WITH a supplied binding (a NULL binding folds to the empty string, 1051 * Oracle's NULL-concatenation semantics), locals with exactly one 1052 * literal-composed assignment, || and parentheses. Null when anything outside 1053 * the whitelist (or an unbound parameter) is involved — never a partial text. 1054 * 1055 * <p>Flow order: a local's single assignment is usable only when it appears 1056 * strictly BEFORE ({@code boundLine},{@code boundCol}) — the use site on the 1057 * first level, then the consuming assignment's own position as the recursion 1058 * descends — so a value assigned after the site can never fabricate lineage. 1059 */ 1060 private static String foldLiteralBindableExpr(gudusoft.gsqlparser.nodes.TExpression expr, 1061 PlsqlBindContext ctx, Map<String, SqlValue> bindings, java.util.Set<String> visiting, 1062 long boundLine, long boundCol) { 1063 if (expr == null) { 1064 return null; 1065 } 1066 if (isCompileTimeLiteral(expr)) { 1067 SqlValue value = literalToSqlValue(expr); 1068 if (value == null) { 1069 return null; 1070 } 1071 return value.isNull() ? "" : String.valueOf(value.text()); 1072 } 1073 gudusoft.gsqlparser.EExpressionType type = expr.getExpressionType(); 1074 if (type == gudusoft.gsqlparser.EExpressionType.concatenate_t) { 1075 String left = foldLiteralBindableExpr(expr.getLeftOperand(), ctx, bindings, visiting, 1076 boundLine, boundCol); 1077 String right = foldLiteralBindableExpr(expr.getRightOperand(), ctx, bindings, visiting, 1078 boundLine, boundCol); 1079 return left == null || right == null ? null : left + right; 1080 } 1081 if (type == gudusoft.gsqlparser.EExpressionType.parenthesis_t) { 1082 return foldLiteralBindableExpr(expr.getLeftOperand(), ctx, bindings, visiting, 1083 boundLine, boundCol); 1084 } 1085 if (type == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 1086 String text = expr.toString(); 1087 if (text == null || SQLUtil.parseNames(text).size() != 1) { 1088 return null; 1089 } 1090 String key = canonVarKey(text); 1091 if (ctx.disallowed.contains(key)) { 1092 return null; 1093 } 1094 if (ctx.params.contains(key)) { 1095 SqlValue bound = bindings.get(key); 1096 if (bound == null) { 1097 return null; // unbound parameter - never fold partially 1098 } 1099 return bound.isNull() ? "" : String.valueOf(bound.text()); 1100 } 1101 List<BoundSource> sources = ctx.assignments.get(key); 1102 if (sources == null || sources.size() != 1 || !visiting.add(key)) { 1103 return null; 1104 } 1105 try { 1106 BoundSource source = sources.get(0); 1107 if (source.line > boundLine 1108 || (source.line == boundLine && source.col >= boundCol)) { 1109 return null; // assigned at or after the use site - value unavailable 1110 } 1111 return foldLiteralBindableExpr(source.expr, ctx, bindings, visiting, 1112 source.line, source.col); 1113 } finally { 1114 visiting.remove(key); 1115 } 1116 } 1117 return null; 1118 } 1119 1120 /** 1121 * Literal call-site binding sets for an Oracle routine, mirroring the T-SQL 1122 * {@link #discoverCallSiteBindings} semantics: same-file callers first (file 1123 * order), then cross-file callers via the catalog (exact-text order); vetted by 1124 * {@link #isVettedLiteralCaller} (so only positional, compile-time-literal 1125 * arguments to a provably-unambiguous target survive); one binding map per 1126 * matching call, deduplicated by canonical key, capped. 1127 */ 1128 static List<Map<String, SqlValue>> discoverPlsqlCallSiteBindings( 1129 TCustomSqlStatement procAst, RoutineCatalog catalog, int maxBindingSets) { 1130 List<Map<String, SqlValue>> bindingSets = new ArrayList<Map<String, SqlValue>>(); 1131 if (maxBindingSets <= 0 || procAst == null || procAst.getGsqlparser() == null) { 1132 return bindingSets; 1133 } 1134 String simple = plsqlRoutineSimpleName(procAst); 1135 if (simple == null || simple.isEmpty()) { 1136 return bindingSets; 1137 } 1138 final PlsqlRoutineIdent ident = new PlsqlRoutineIdent(procAst, simple); 1139 TStatementList ownStatements = procAst.getGsqlparser().getSqlstatements(); 1140 String ownText = procAst.getGsqlparser().sqltext; 1141 // Same-FILE duplicate definitions make the name just as ambiguous as foreign 1142 // ones (the interpretation path catches this via its multi-ident matching; 1143 // this single-ident path must check explicitly). 1144 if (ownStatements != null) { 1145 for (int i = 0; i < ownStatements.size(); i++) { 1146 TCustomSqlStatement stmt = ownStatements.get(i); 1147 if (stmt == procAst) { 1148 continue; 1149 } 1150 String otherSimple = plsqlRoutineSimpleName(stmt); 1151 if (otherSimple == null || !SQLUtil.sameName(EDbVendor.dbvoracle, 1152 ESQLDataObjectType.dotProcedure, otherSimple, ident.simple)) { 1153 continue; 1154 } 1155 ident.hasForeignSameName = true; 1156 if (!provablyDifferentSchema(ident.schema, plsqlRoutineSchema(stmt))) { 1157 ident.ambiguous = true; 1158 } 1159 } 1160 } 1161 // Foreign definitions decide the ambiguity verdicts (same rules as the 1162 // interpretation path). 1163 List<RoutineCatalog.UnitIndex> foreignUnits = new ArrayList<RoutineCatalog.UnitIndex>(); 1164 if (catalog != null) { 1165 String probe = bareName(simple).toLowerCase(java.util.Locale.ENGLISH); 1166 for (String text : catalog.unitTexts()) { 1167 if (text == null || text.equals(ownText) 1168 || !text.toLowerCase(java.util.Locale.ENGLISH).contains(probe)) { 1169 continue; 1170 } 1171 RoutineCatalog.UnitIndex unit = catalog.indexOf(text); 1172 if (unit == null) { 1173 continue; 1174 } 1175 foreignUnits.add(unit); 1176 for (RoutineCatalog.Definition def : unit.definitions) { 1177 if (!SQLUtil.sameName(EDbVendor.dbvoracle, ESQLDataObjectType.dotProcedure, 1178 def.simple, ident.simple)) { 1179 continue; 1180 } 1181 ident.hasForeignSameName = true; 1182 if (!provablyDifferentSchema(ident.schema, def.schema)) { 1183 ident.ambiguous = true; 1184 } 1185 } 1186 } 1187 } 1188 List<PlsqlRoutineIdent> idents = new ArrayList<PlsqlRoutineIdent>(); 1189 idents.add(ident); 1190 List<TParameterDeclaration> params = new ArrayList<TParameterDeclaration>(); 1191 if (procAst instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) { 1192 TParameterDeclarationList declared = ((gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) procAst) 1193 .getParameterDeclarations(); 1194 if (declared != null) { 1195 for (int i = 0; i < declared.size(); i++) { 1196 params.add(declared.getParameterDeclarationItem(i)); 1197 } 1198 } 1199 } 1200 java.util.Set<String> seen = new java.util.HashSet<String>(); 1201 // Same-file callers in file order. 1202 if (ownStatements != null) { 1203 for (int i = 0; i < ownStatements.size() 1204 && bindingSets.size() < maxBindingSets; i++) { 1205 TCustomSqlStatement stmt = ownStatements.get(i); 1206 if (stmt == procAst || plsqlRoutineSimpleName(stmt) != null 1207 || isPlsqlDefinitionShape(stmt)) { 1208 continue; 1209 } 1210 if (isVettedLiteralCaller(stmt, idents)) { 1211 extractPlsqlBindings(stmt, ident, params, seen, bindingSets, maxBindingSets); 1212 } 1213 } 1214 } 1215 // Cross-file callers ordered by exact statement text. 1216 java.util.TreeMap<String, TCustomSqlStatement> ordered = 1217 new java.util.TreeMap<String, TCustomSqlStatement>(); 1218 for (RoutineCatalog.UnitIndex unit : foreignUnits) { 1219 for (RoutineCatalog.CallCandidate candidate : unit.callCandidates) { 1220 if (isPlsqlDefinitionShape(candidate.stmt)) { 1221 continue; 1222 } 1223 if (isVettedLiteralCaller(candidate.stmt, idents)) { 1224 String key = String.valueOf(candidate.stmt.toString()); 1225 if (!ordered.containsKey(key)) { 1226 ordered.put(key, candidate.stmt); 1227 } 1228 } 1229 } 1230 } 1231 for (TCustomSqlStatement stmt : ordered.values()) { 1232 if (bindingSets.size() >= maxBindingSets) { 1233 break; 1234 } 1235 extractPlsqlBindings(stmt, ident, params, seen, bindingSets, maxBindingSets); 1236 } 1237 return bindingSets; 1238 } 1239 1240 /** 1241 * Extract one binding map per call to {@code ident} inside a VETTED caller 1242 * statement. Vetting already guaranteed every matching call is unambiguous and 1243 * all-literal (positional form — named notation is rejected there), so this 1244 * maps arguments to parameter declarations by position. 1245 */ 1246 private static void extractPlsqlBindings(TCustomSqlStatement callerStmt, 1247 final PlsqlRoutineIdent ident, final List<TParameterDeclaration> params, 1248 final java.util.Set<String> seen, final List<Map<String, SqlValue>> bindingSets, 1249 final int maxBindingSets) { 1250 try { 1251 callerStmt.acceptChildren(new gudusoft.gsqlparser.nodes.TParseTreeVisitor() { 1252 public void preVisit(gudusoft.gsqlparser.nodes.TFunctionCall call) { 1253 collect(call); 1254 } 1255 1256 public void preVisit(gudusoft.gsqlparser.stmt.TCallStatement call) { 1257 if (call.getRoutineExpr() != null) { 1258 collect(call.getRoutineExpr().getFunctionCall()); 1259 } 1260 } 1261 1262 private void collect(gudusoft.gsqlparser.nodes.TFunctionCall call) { 1263 if (call == null || call.getFunctionName() == null 1264 || bindingSets.size() >= maxBindingSets) { 1265 return; 1266 } 1267 List<String> parts = SQLUtil.parseNames(call.getFunctionName().toString()); 1268 if (parts.isEmpty() || !SQLUtil.sameName(EDbVendor.dbvoracle, 1269 ESQLDataObjectType.dotProcedure, parts.get(parts.size() - 1), 1270 ident.simple)) { 1271 return; 1272 } 1273 if (call.getArgs() == null || call.getArgs().size() == 0) { 1274 return; 1275 } 1276 Map<String, SqlValue> bindings = new java.util.LinkedHashMap<String, SqlValue>(); 1277 java.util.TreeMap<String, String> canonical = new java.util.TreeMap<String, String>(); 1278 for (int i = 0; i < call.getArgs().size(); i++) { 1279 if (i >= params.size() || params.get(i) == null 1280 || params.get(i).getParameterName() == null) { 1281 return; // arity beyond declaration - do not guess 1282 } 1283 SqlValue value = literalToSqlValue(call.getArgs().getExpression(i)); 1284 if (value == null) { 1285 return; // vetting should prevent this; stay safe 1286 } 1287 String paramName = params.get(i).getParameterName().toString(); 1288 bindings.put(canonVarKey(paramName), value); 1289 canonical.put(canonVarKey(paramName), 1290 canonicalValueKey(call.getArgs().getExpression(i))); 1291 } 1292 if (!bindings.isEmpty() && seen.add(canonical.toString())) { 1293 bindingSets.add(bindings); 1294 } 1295 } 1296 }); 1297 } catch (RuntimeException ex) { 1298 // best-effort extraction; a crashed walk contributes nothing 1299 } 1300 } 1301 1302 /** 1303 * Variable-tracking key preserving Oracle quote semantics: unquoted names fold 1304 * upper (case-insensitive), quoted names keep their exact case inside markers so 1305 * a quoted lowercase local is never conflated with an unquoted parameter of the 1306 * same spelling. (Unquoted FOO vs quoted "FOO" key differently - a conservative 1307 * miss that can only disqualify, never wrongly qualify.) 1308 */ 1309 private static String canonVarKey(String name) { 1310 String trimmed = name.trim(); 1311 if (trimmed.length() >= 2 && trimmed.charAt(0) == '"' 1312 && trimmed.charAt(trimmed.length() - 1) == '"') { 1313 return "\"" + trimmed.substring(1, trimmed.length() - 1) + "\""; 1314 } 1315 return trimmed.toUpperCase(java.util.Locale.ENGLISH); 1316 } 1317 1318 /** 1319 * Recursive whitelist for {@link #plsqlDynamicSqlLiteralBindable}: literals, 1320 * || concatenation, parentheses, routine parameters, and local variables whose 1321 * every recorded assignment is itself bindable. Cycles and everything else fail. 1322 */ 1323 private static boolean isLiteralBindableExpr(gudusoft.gsqlparser.nodes.TExpression expr, 1324 java.util.Set<String> params, 1325 Map<String, List<BoundSource>> assignments, 1326 java.util.Set<String> disallowed, java.util.Set<String> visiting) { 1327 if (expr == null) { 1328 return false; 1329 } 1330 if (isCompileTimeLiteral(expr)) { 1331 return true; 1332 } 1333 gudusoft.gsqlparser.EExpressionType type = expr.getExpressionType(); 1334 if (type == gudusoft.gsqlparser.EExpressionType.concatenate_t) { 1335 return isLiteralBindableExpr(expr.getLeftOperand(), params, assignments, disallowed, visiting) 1336 && isLiteralBindableExpr(expr.getRightOperand(), params, assignments, disallowed, visiting); 1337 } 1338 if (type == gudusoft.gsqlparser.EExpressionType.parenthesis_t) { 1339 return isLiteralBindableExpr(expr.getLeftOperand(), params, assignments, disallowed, visiting); 1340 } 1341 if (type == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 1342 String text = expr.toString(); 1343 if (text == null || SQLUtil.parseNames(text).size() != 1) { 1344 return false; 1345 } 1346 String key = canonVarKey(text); 1347 if (disallowed.contains(key)) { 1348 return false; 1349 } 1350 if (params.contains(key)) { 1351 return true; 1352 } 1353 List<BoundSource> sources = assignments.get(key); 1354 // Exactly ONE recorded source (assignment or declared default): with 1355 // several, control flow (IF/loops) selects which one is live, but the 1356 // interpreter walks structurally and would collapse them to a single 1357 // arbitrary winner - presenting one possible lineage as certain. Real 1358 // branch-sensitive evaluation is Phase 3 CFG work. 1359 if (sources == null || sources.size() != 1 || !visiting.add(key)) { 1360 return false; 1361 } 1362 try { 1363 if (!isLiteralBindableExpr(sources.get(0).expr, params, assignments, 1364 disallowed, visiting)) { 1365 return false; 1366 } 1367 } finally { 1368 visiting.remove(key); 1369 } 1370 return true; 1371 } 1372 return false; 1373 } 1374 1375 /** Compile-time literal: string/numeric constant, signed numeric, or bare NULL. */ 1376 private static boolean isCompileTimeLiteral(gudusoft.gsqlparser.nodes.TExpression expr) { 1377 if (expr == null) { 1378 return false; 1379 } 1380 gudusoft.gsqlparser.EExpressionType type = expr.getExpressionType(); 1381 if (type == gudusoft.gsqlparser.EExpressionType.unary_plus_t 1382 || type == gudusoft.gsqlparser.EExpressionType.unary_minus_t) { 1383 return isCompileTimeLiteral(expr.getRightOperand() != null 1384 ? expr.getRightOperand() : expr.getLeftOperand()); 1385 } 1386 if (type == gudusoft.gsqlparser.EExpressionType.simple_source_token_t 1387 && expr.toString().trim().equalsIgnoreCase("NULL")) { // non-identifier-compare: the NULL keyword literal 1388 return true; 1389 } 1390 return type == gudusoft.gsqlparser.EExpressionType.simple_constant_t 1391 && expr.getConstantOperand() != null; 1392 } 1393 1394 /** Simple name of an Oracle routine definition; null for any other statement. */ 1395 private static String plsqlRoutineSimpleName(TCustomSqlStatement stmt) { 1396 gudusoft.gsqlparser.nodes.TObjectName name = null; 1397 if (stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) { 1398 name = ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) stmt).getProcedureName(); 1399 } else if (stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) { 1400 name = ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) stmt).getFunctionName(); 1401 } 1402 if (name == null) { 1403 return null; 1404 } 1405 return lastNameSegment(name.toString()); 1406 } 1407 1408 // Package-visible accessors for RoutineCatalog (plan §5.6 index). Same helpers, 1409 // exposed under index-facing names; no behavior of their own. 1410 static String mssqlDefinitionDatabase(TMssqlCreateProcedure def, String currentDb) { 1411 return definitionDatabase(def, currentDb); 1412 } 1413 1414 static String simpleNameOf(String name) { 1415 return lastNameSegment(name); 1416 } 1417 1418 static String plsqlRoutineSimpleNameOf(TCustomSqlStatement stmt) { 1419 return plsqlRoutineSimpleName(stmt); 1420 } 1421 1422 static String plsqlRoutineSchemaOf(TCustomSqlStatement stmt) { 1423 return plsqlRoutineSchema(stmt); 1424 } 1425 1426 /** Effective database (last USE before the definition) of {@code procAst} in its file. */ 1427 private static String effectiveDatabaseOfProc(TStatementList all, TCustomSqlStatement procAst) { 1428 String currentDb = null; 1429 for (int i = 0; i < all.size(); i++) { 1430 if (all.get(i) instanceof gudusoft.gsqlparser.stmt.TUseDatabase) { 1431 currentDb = String.valueOf(((gudusoft.gsqlparser.stmt.TUseDatabase) all.get(i)).getDatabaseName()); 1432 } 1433 if (all.get(i) == procAst) { 1434 return currentDb; 1435 } 1436 } 1437 return null; 1438 } 1439 1440 /** 1441 * True when {@code call} is a plain (non-dynamic-text) EXEC of {@code proc} under 1442 * the matching rules: unqualified operands match by simple name, qualified operands 1443 * require the full qualified identity (so EXEC audit.load is never taken as a call 1444 * to dbo.load), and one-/two-part names must agree on the effective USE database. 1445 */ 1446 private static boolean callMatchesProc(gudusoft.gsqlparser.stmt.mssql.TMssqlExecute call, 1447 TMssqlCreateProcedure proc, String procSimpleName, String procDb, String callDb) { 1448 if (call.getModuleName() == null || call.getSqlText() != null) { 1449 return false; 1450 } 1451 String callName = call.getModuleName().toString(); 1452 if (!sameEffectiveDatabase(procDb, callDb, callName)) { 1453 return false; 1454 } 1455 List<String> callParts = SQLUtil.parseNames(callName); 1456 if (callParts.size() <= 1) { 1457 return SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotProcedure, 1458 lastNameSegment(callName), procSimpleName); 1459 } 1460 List<String> procParts = SQLUtil.parseNames(proc.getProcedureName().toString()); 1461 if (callParts.size() == procParts.size()) { 1462 return SQLUtil.compareIdentifier(EDbVendor.dbvmssql, ESQLDataObjectType.dotProcedure, 1463 callName, proc.getProcedureName().toString()); 1464 } 1465 if (callParts.size() == 2 && procParts.size() == 3) { 1466 // Two-part call to a three-part definition: database agreement was already 1467 // established by sameEffectiveDatabase (call's USE db vs the definition's 1468 // catalog); compare the remaining schema.name segments. 1469 return SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotSchema, 1470 callParts.get(0), procParts.get(1)) 1471 && SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotProcedure, 1472 callParts.get(1), procParts.get(2)); 1473 } 1474 if (callParts.size() == 3 && procParts.size() == 2) { 1475 // Three-part call to a two-part definition: the call's explicit catalog 1476 // must equal the definition's known effective database. 1477 return procDb != null 1478 && SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotCatalog, 1479 callParts.get(0), procDb) 1480 && SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotSchema, 1481 callParts.get(1), procParts.get(0)) 1482 && SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotProcedure, 1483 callParts.get(2), procParts.get(1)); 1484 } 1485 // Other arity combinations (e.g. one-part definition vs qualified call) keep 1486 // the conservative no-match: schema defaulting is environment-dependent. 1487 return false; 1488 } 1489 1490 /** A literal binding set extracted from one call site plus its canonical dedupe key. */ 1491 private static final class BindingCandidate { 1492 final Map<String, SqlValue> bindings; 1493 final String canonicalKey; 1494 1495 BindingCandidate(Map<String, SqlValue> bindings, String canonicalKey) { 1496 this.bindings = bindings; 1497 this.canonicalKey = canonicalKey; 1498 } 1499 } 1500 1501 /** 1502 * Convert one EXEC call's arguments into a binding set; null when nothing literal 1503 * was bound or the call passes a runtime value to a DEFAULTED formal (the declared 1504 * default would otherwise mask the unknown runtime value). 1505 */ 1506 private static BindingCandidate bindingCandidateFromCall( 1507 gudusoft.gsqlparser.stmt.mssql.TMssqlExecute call, List<TParameterDeclaration> params) { 1508 Map<String, SqlValue> bindings = new java.util.LinkedHashMap<String, SqlValue>(); 1509 java.util.TreeMap<String, String> canonical = new java.util.TreeMap<String, String>(); 1510 if (call.getParameters() != null) { 1511 for (int j = 0; j < call.getParameters().size(); j++) { 1512 gudusoft.gsqlparser.nodes.TExecParameter arg = call.getParameters().getExecParameter(j); 1513 if (arg == null || arg.getParameterMode() != TBaseType.parameter_mode_default) { 1514 // OUT/OUTPUT actuals stay unbound: the value at the dynamic 1515 // site may differ from the literal passed in. 1516 continue; 1517 } 1518 String paramName = null; 1519 if (arg.getParameterName() != null) { 1520 paramName = arg.getParameterName().toString(); 1521 } else if (j < params.size() && params.get(j).getParameterName() != null) { 1522 paramName = params.get(j).getParameterName().toString(); 1523 } 1524 SqlValue value = literalToSqlValue(arg.getParameterValue()); 1525 if (paramName != null && value == null && hasLiteralDefault(params, paramName)) { 1526 // Explicitly-supplied runtime value for a DEFAULTED formal: the 1527 // evaluator would wrongly seed the declared default for it. 1528 // This call site cannot be represented soundly - drop it. 1529 return null; 1530 } 1531 if (paramName != null && value != null) { 1532 bindings.put(paramName, value); 1533 // Canonical dedupe key: normalized name -> typed escaped value, 1534 // insensitive to argument order, @-prefix and case. 1535 String normalized = paramName.startsWith("@") ? paramName.substring(1) : paramName; 1536 canonical.put(normalized.toUpperCase(java.util.Locale.ENGLISH), 1537 canonicalValueKey(arg.getParameterValue())); 1538 } 1539 } 1540 } 1541 if (bindings.isEmpty()) { 1542 return null; 1543 } 1544 return new BindingCandidate(bindings, canonical.toString()); 1545 } 1546 1547 /** 1548 * Database a definition belongs to: the catalog part of a three-part name wins, 1549 * otherwise the effective USE database at the definition site. 1550 */ 1551 private static String definitionDatabase(TMssqlCreateProcedure def, String currentDb) { 1552 List<String> parts = SQLUtil.parseNames(def.getProcedureName().toString()); 1553 if (parts.size() >= 3) { 1554 return parts.get(0); 1555 } 1556 return currentDb; 1557 } 1558 1559 /** 1560 * True only when the two database contexts are BOTH known and differ. A null on 1561 * either side means "analysis default" - possibly the same database - so it is 1562 * NOT provably different (the conservative direction for the ambiguity bail-out). 1563 */ 1564 private static boolean provablyDifferentDatabase(String dbA, String dbB) { 1565 if (dbA == null || dbB == null) { 1566 return false; 1567 } 1568 return !SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotCatalog, dbA, dbB); 1569 } 1570 1571 /** Name stripped of [..] / ".." / `..` quoting, for the cheap contains-prefilter only. */ 1572 private static String bareName(String name) { 1573 String trimmed = name.trim(); 1574 if (trimmed.length() >= 2) { 1575 char first = trimmed.charAt(0); 1576 char last = trimmed.charAt(trimmed.length() - 1); 1577 if ((first == '[' && last == ']') || (first == '"' && last == '"') 1578 || (first == '`' && last == '`')) { 1579 return trimmed.substring(1, trimmed.length() - 1).trim(); 1580 } 1581 } 1582 return trimmed; 1583 } 1584 1585 1586 /** 1587 * True when the call's effective database matches the procedure definition's. An 1588 * explicit three-part call name carries its own database and was already compared 1589 * as a full qualified identity; here we only reject cross-USE ambiguity for one- 1590 * and two-part names. 1591 */ 1592 private static boolean sameEffectiveDatabase(String procDb, String callDb, String callName) { 1593 if (SQLUtil.parseNames(callName).size() >= 3) { 1594 return true; 1595 } 1596 if (procDb == null && callDb == null) { 1597 return true; 1598 } 1599 if (procDb == null || callDb == null) { 1600 return false; 1601 } 1602 return SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotCatalog, procDb, callDb); 1603 } 1604 1605 private static boolean hasLiteralDefault(List<TParameterDeclaration> params, String paramName) { 1606 for (TParameterDeclaration param : params) { 1607 if (param.getParameterName() != null 1608 && SQLUtil.sameName(EDbVendor.dbvmssql, ESQLDataObjectType.dotColumn, 1609 param.getParameterName().toString(), paramName)) { 1610 return param.getDefaultValue() != null; 1611 } 1612 } 1613 return false; 1614 } 1615 1616 private static String lastNameSegment(String name) { 1617 List<String> parts = SQLUtil.parseNames(name); 1618 return parts.isEmpty() ? name : parts.get(parts.size() - 1); 1619 } 1620 1621 /** T-SQL literal -> binding value; null when the expression is not a compile-time literal. */ 1622 private static SqlValue literalToSqlValue(gudusoft.gsqlparser.nodes.TExpression expr) { 1623 if (expr == null) { 1624 return null; 1625 } 1626 gudusoft.gsqlparser.EExpressionType type = expr.getExpressionType(); 1627 // Signed numerics arrive as unary +/- wrapping the constant. 1628 if (type == gudusoft.gsqlparser.EExpressionType.unary_plus_t 1629 || type == gudusoft.gsqlparser.EExpressionType.unary_minus_t) { 1630 SqlValue inner = literalToSqlValue(expr.getRightOperand() != null 1631 ? expr.getRightOperand() : expr.getLeftOperand()); 1632 if (inner == null || type == gudusoft.gsqlparser.EExpressionType.unary_plus_t) { 1633 return inner; 1634 } 1635 try { 1636 String innerText = inner.text() == null ? "" : inner.text(); 1637 if (innerText.indexOf('.') >= 0) { 1638 return SqlValue.of(-Double.parseDouble(innerText)); 1639 } 1640 return SqlValue.of(-Long.parseLong(innerText)); 1641 } catch (NumberFormatException nfe) { 1642 return null; 1643 } 1644 } 1645 // Bare NULL in an EXEC argument surfaces as a plain source token. 1646 if (type == gudusoft.gsqlparser.EExpressionType.simple_source_token_t 1647 && expr.toString().trim().equalsIgnoreCase("NULL")) { // non-identifier-compare: the NULL keyword literal 1648 return SqlValue.nullValue(); 1649 } 1650 if (type != gudusoft.gsqlparser.EExpressionType.simple_constant_t 1651 || expr.getConstantOperand() == null) { 1652 return null; 1653 } 1654 String text = expr.getConstantOperand().toString().trim(); 1655 if (text.equalsIgnoreCase("NULL")) { // non-identifier-compare: the NULL keyword literal 1656 return SqlValue.nullValue(); 1657 } 1658 String unquoted = unquoteTsqlString(text); 1659 if (unquoted != null) { 1660 return SqlValue.of(unquoted); 1661 } 1662 try { 1663 if (text.indexOf('.') >= 0 || text.indexOf('e') >= 0 || text.indexOf('E') >= 0) { 1664 // Bind decimals only when the double round-trips to the exact source 1665 // text - otherwise the evaluated string could differ from the real 1666 // runtime value (precision loss); leave inexact values unbound. 1667 double parsed = Double.parseDouble(text); 1668 SqlValue candidate = SqlValue.of(parsed); 1669 if (text.equals(candidate.text())) { 1670 return candidate; 1671 } 1672 return null; 1673 } 1674 return SqlValue.of(Long.parseLong(text)); 1675 } catch (NumberFormatException nfe) { 1676 return null; 1677 } 1678 } 1679 1680 /** 'abc''d' / N'abc' -> abc'd ; returns null when not a string literal. */ 1681 private static String unquoteTsqlString(String text) { 1682 String t = text; 1683 if (t.length() >= 2 && (t.charAt(0) == 'N' || t.charAt(0) == 'n') && t.charAt(1) == '\'') { 1684 t = t.substring(1); 1685 } 1686 if (t.length() >= 2 && t.charAt(0) == '\'' && t.charAt(t.length() - 1) == '\'') { 1687 return t.substring(1, t.length() - 1).replace("''", "'"); 1688 } 1689 return null; 1690 } 1691 1692 /** Order/spelling-insensitive value key for binding-set dedupe (length-prefixed to defeat collisions). */ 1693 private static String canonicalValueKey(gudusoft.gsqlparser.nodes.TExpression expr) { 1694 String raw = expr == null ? "" : expr.toString().trim(); 1695 return raw.length() + ":" + raw; 1696 } 1697 1698 /* --------------------------------------------------------- site building */ 1699 1700 private static DynamicSiteResult buildSite(TDynamicSqlStringEvaluator.RawSite raw, EDbVendor vendor, 1701 TSQLEnv sqlEnv, String currentDatabase, String defaultSchema, String sourceProc, String bindingHash, 1702 DynamicLineageOptions options) { 1703 1704 long[] pos = coordinate(raw.node); 1705 String dynamicSite = dynamicSiteId(sourceProc, 1706 raw.ordinalPath + (raw.variantPath == null ? "" : raw.variantPath)); 1707 SqlStringValue sql = raw.sql; 1708 // The EXEC site is wrapped in an IF whose predicate we cannot decide from 1709 // the bindings (e.g. IF @Debug = 0 EXEC(@sql) with @Debug unbound). 1710 boolean executionGated = !raw.controlFlowCertain; 1711 DynamicSqlTrustMode trustMode = options.getTrustMode(); 1712 HoleFacts holes = holeFacts(sql); 1713 1714 // Honest unresolved reasons — never guess. 1715 // Case (a): the dynamic string itself is not usable — NULL, opaque, or 1716 // assigned under an undecidable branch and collapsed to UNKNOWN. No 1717 // usable inner-SQL candidate exists for this site. 1718 if (sql == null || !sql.usable()) { 1719 String reason = sql != null && sql.reason != null ? sql.reason 1720 : executionGated ? "site guarded by an undecidable conditional" 1721 : "dynamic SQL did not reduce to a concrete string"; 1722 return unresolved(raw, dynamicSite, sourceProc, bindingHash, pos, reason, trustMode, holes); 1723 } 1724 // Case (b): the EXEC site is execution-gated, but the string itself is usable 1725 // (CONCRETE or PARTIAL) — only the *did-it-run* fact is uncertain, so we 1726 // degrade-don't-fail rather than dropping the lineage. We fall through to 1727 // analyzeResolvedSql either way: a fully-concrete gated site emits all its 1728 // edges (flagged CONDITIONAL below), and a PARTIAL gated site emits exactly 1729 // the placeholder-free subset a non-gated PARTIAL site would, suppressing only 1730 // the placeholder edges. The downstream status/reason block surfaces both the 1731 // placeholder suppression and the execution gate. (Case (a) — an unusable 1732 // string, NULL/UNKNOWN — was already returned UNRESOLVED above.) 1733 String resolvedSql = sql.text; 1734 if (resolvedSql.length() > options.getMaxResolvedSqlLength()) { 1735 return unresolved(raw, dynamicSite, sourceProc, bindingHash, pos, 1736 "resolved SQL length " + resolvedSql.length() + " exceeds maxResolvedSqlLength", 1737 trustMode, holes); 1738 } 1739 1740 String concreteHash = sha1Hex(resolvedSql); 1741 boolean partialPlaceholders = sql.state == SqlStringValue.State.PARTIAL; 1742 boolean provenanceIncomplete = !hasCompleteLiteralProvenance(sql.text, sql.fragments); 1743 1744 List<DynamicLineageEdge> edges; 1745 boolean parseError; 1746 boolean partialParseError; 1747 boolean nestedIncomplete; 1748 try { 1749 Analysis a = analyzeResolvedSql(resolvedSql, vendor, sqlEnv, currentDatabase, defaultSchema, 1750 sourceProc, dynamicSite, bindingHash, options, sql.fragments); 1751 edges = a.edges; 1752 nestedIncomplete = a.nestedIncomplete; 1753 // No lineage + recorded errors = parse failure (generateDataFlow() 1754 // does not throw on bad SQL). Errors WITH some lineage = a mixed batch 1755 // where one statement failed; keep the good edges but flag it honestly. 1756 parseError = a.hadErrors && a.edges.isEmpty(); 1757 partialParseError = a.hadErrors && !a.edges.isEmpty(); 1758 } catch (RuntimeException ex) { 1759 edges = new ArrayList<DynamicLineageEdge>(); 1760 parseError = true; 1761 partialParseError = false; 1762 nestedIncomplete = false; 1763 } 1764 1765 boolean aggregateIncomplete = provenanceIncomplete || nestedIncomplete 1766 || (partialParseError && trustMode == DynamicSqlTrustMode.SHADOW); 1767 HoleFacts aggregateHoles = nestedIncomplete ? new HoleFacts(-1, false) : holes; 1768 1769 DynamicSiteResult.Status status; 1770 if (parseError) { 1771 status = DynamicSiteResult.Status.PARSE_ERROR; 1772 } else if (executionGated) { 1773 // The undecidable gate dominates the status: whether the string is fully 1774 // concrete or PARTIAL, every emitted edge is reached only if the guarded 1775 // branch runs. CONDITIONAL keeps that "might not execute" caveat visible — 1776 // the strictly-stronger signal to preserve — and the reason below adds the 1777 // placeholder-suppression note when the string is also PARTIAL. 1778 status = DynamicSiteResult.Status.CONDITIONAL; 1779 } else if (partialPlaceholders 1780 || (aggregateIncomplete && trustMode == DynamicSqlTrustMode.SHADOW)) { 1781 status = DynamicSiteResult.Status.PARTIAL; 1782 } else { 1783 status = DynamicSiteResult.Status.RESOLVED; 1784 } 1785 1786 // Compose the reason from every applicable fact rather than picking one — a 1787 // single site can be partial-parse AND placeholder-suppressed AND gated at 1788 // once, and a consumer needs all three to gauge how far to trust the edges. 1789 List<String> reasons = new ArrayList<String>(); 1790 if (parseError) { 1791 reasons.add("materialized SQL failed to parse"); 1792 } else if (partialParseError) { 1793 reasons.add("some statements in the materialized SQL failed to parse; emitted edges are from the parseable statements"); 1794 } 1795 if (partialPlaceholders) { 1796 reasons.add("materialized SQL still contains unbound placeholder(s); placeholder edges suppressed"); 1797 } 1798 int unprovenRelationshipCount = 0; 1799 if (nestedIncomplete) { 1800 reasons.add("nested dynamic SQL is incomplete"); 1801 } 1802 if (aggregateIncomplete && trustMode == DynamicSqlTrustMode.SHADOW) { 1803 unprovenRelationshipCount = edges.size(); 1804 reasons.add("SHADOW retained " + unprovenRelationshipCount 1805 + " historical candidate relationship(s) without a per-edge commitment proof; " 1806 + holeDescription(aggregateHoles)); 1807 } 1808 if (executionGated) { 1809 boolean anyEdges = !edges.isEmpty(); 1810 reasons.add("site reached through an undecidable conditional" 1811 + (anyEdges ? " — emitted edges assume the guarded statement executed" 1812 : " — no resolvable edges were produced")); 1813 } 1814 String reason = reasons.isEmpty() ? null : String.join("; ", reasons); 1815 1816 return new DynamicSiteResult(raw.kind, status, resolvedSql, concreteHash, sourceProc, dynamicSite, 1817 bindingHash, reason, pos[0], pos[1], edges, trustMode, 1818 unprovenRelationshipCount, aggregateHoles.count, aggregateHoles.exact, 1819 raw.diagnostic); 1820 } 1821 1822 private static DynamicSiteResult unresolved(TDynamicSqlStringEvaluator.RawSite raw, String dynamicSite, 1823 String sourceProc, String bindingHash, long[] pos, String reason, 1824 DynamicSqlTrustMode trustMode, HoleFacts holes) { 1825 String text = raw.sql != null && raw.sql.usable() ? raw.sql.text : null; 1826 return new DynamicSiteResult(raw.kind, DynamicSiteResult.Status.UNRESOLVED, text, 1827 text != null ? sha1Hex(text) : null, sourceProc, dynamicSite, bindingHash, reason, 1828 pos[0], pos[1], new ArrayList<DynamicLineageEdge>(), trustMode, 1829 0, holes.count, holes.exact, raw.diagnostic); 1830 } 1831 1832 private static final class HoleFacts { 1833 final int count; 1834 final boolean exact; 1835 1836 HoleFacts(int count, boolean exact) { 1837 this.count = count; 1838 this.exact = exact; 1839 } 1840 } 1841 1842 private static HoleFacts holeFacts(SqlStringValue sql) { 1843 if (sql == null) { 1844 return new HoleFacts(-1, false); 1845 } 1846 int count = 0; 1847 for (SqlFragment fragment : sql.fragments) { 1848 if (!fragment.isLiteral()) { 1849 count++; 1850 } 1851 } 1852 if (sql.usable() && sql.fragments.isEmpty()) { 1853 return new HoleFacts(-1, false); 1854 } 1855 if (!sql.usable() && count == 0) { 1856 // The value did not reduce, yet its provenance shows no hole: whatever 1857 // stopped the fold left no fragment. Not known - never an exact zero. 1858 return new HoleFacts(-1, false); 1859 } 1860 return new HoleFacts(count, true); 1861 } 1862 1863 private static String holeDescription(HoleFacts holes) { 1864 return holes.exact ? holes.count + " unresolved fragment(s)" 1865 : "unresolved fragment count unavailable from the producer"; 1866 } 1867 1868 /** Complete means usable text with non-empty, exclusively literal provenance. */ 1869 private static boolean hasCompleteLiteralProvenance(String text, List<SqlFragment> fragments) { 1870 if (text == null || fragments == null || fragments.isEmpty()) { 1871 return false; 1872 } 1873 StringBuilder rendered = new StringBuilder(); 1874 for (SqlFragment fragment : fragments) { 1875 if (fragment == null || !fragment.isLiteral()) { 1876 return false; 1877 } 1878 rendered.append(fragment.text); 1879 } 1880 return rendered.toString().equals(text); 1881 } 1882 1883 /** 1884 * Shared "analyze a materialized dynamic-SQL string" routine: run ordinary 1885 * dlineage with the proc's database/schema context and lift each column edge 1886 * into a provenance-tagged {@link DynamicLineageEdge}. 1887 */ 1888 /** Edges from analyzing one materialized string, plus whether dlineage recorded parse/analysis errors. */ 1889 private static final class Analysis { 1890 final List<DynamicLineageEdge> edges; 1891 final boolean hadErrors; 1892 final boolean nestedIncomplete; 1893 1894 Analysis(List<DynamicLineageEdge> edges, boolean hadErrors, boolean nestedIncomplete) { 1895 this.edges = edges; 1896 this.hadErrors = hadErrors; 1897 this.nestedIncomplete = nestedIncomplete; 1898 } 1899 } 1900 1901 private static Analysis analyzeResolvedSql(String resolvedSql, EDbVendor vendor, 1902 TSQLEnv sqlEnv, String currentDatabase, String defaultSchema, String sourceProc, String dynamicSite, 1903 String bindingHash, DynamicLineageOptions options, 1904 List<SqlFragment> valueProvenance) { 1905 1906 Option opt = new Option(); 1907 opt.setVendor(vendor); 1908 opt.setSimpleOutput(false); 1909 opt.setOutput(false); 1910 opt.setShowImplicitSchema(options.isShowImplicitSchema()); 1911 opt.setDynamicSqlTrustMode(options.getTrustMode()); 1912 if (currentDatabase != null && !currentDatabase.isEmpty()) { 1913 opt.setDefaultDatabase(currentDatabase); 1914 } 1915 if (defaultSchema != null && !defaultSchema.isEmpty()) { 1916 opt.setDefaultSchema(defaultSchema); 1917 } 1918 1919 DataFlowAnalyzer nested = new DataFlowAnalyzer(resolvedSql, opt); 1920 if (sqlEnv != null) { 1921 nested.setSqlEnv(sqlEnv); 1922 } 1923 nested.generateDataFlow(); 1924 dataflow df = nested.getDataFlow(); 1925 boolean nestedIncomplete = hasIncompleteNestedDynamicSql(nested, options.getTrustMode()); 1926 1927 List<DynamicLineageEdge> edges = new ArrayList<DynamicLineageEdge>(); 1928 if (df == null) { 1929 return new Analysis(edges, true, nestedIncomplete); 1930 } 1931 boolean hadErrors = df.getErrors() != null && !df.getErrors().isEmpty(); 1932 if (df.getRelationships() == null) { 1933 return new Analysis(edges, hadErrors, nestedIncomplete); 1934 } 1935 1936 // Index the real tables/views by id so we can rebuild fully-qualified 1937 // names from the structured model and fill in the proc's database/schema 1938 // for names the dynamic SQL left unqualified. 1939 Map<String, table> realTables = new HashMap<String, table>(); 1940 indexTables(realTables, df.getTables()); 1941 indexTables(realTables, df.getViews()); 1942 1943 boolean showImplicit = options.isShowImplicitSchema(); 1944 for (relationship rel : df.getRelationships()) { 1945 if (rel == null || rel.getTarget() == null || rel.getSources() == null) { 1946 continue; 1947 } 1948 String target = fullName(rel.getTarget(), realTables, currentDatabase, defaultSchema, showImplicit); 1949 // An edge touching an unresolved placeholder (a name still carrying a 1950 // @variable from an unbound binding) must NOT be emitted as resolved 1951 // lineage — it would be a guess. The site stays PARTIAL and its 1952 // resolvedSql still shows the placeholder; only the fully-resolved 1953 // edges flow out. 1954 if (target == null || referencesPlaceholder(target)) { 1955 continue; 1956 } 1957 for (sourceColumn src : rel.getSources()) { 1958 if (isSystem(src)) { 1959 continue; 1960 } 1961 String source = fullName(src, realTables, currentDatabase, defaultSchema, showImplicit); 1962 if (source == null || referencesPlaceholder(source)) { 1963 continue; 1964 } 1965 edges.add(new DynamicLineageEdge(source, target, rel.getEffectType(), rel.getType(), 1966 sourceProc, dynamicSite, bindingHash, valueProvenance)); 1967 } 1968 } 1969 return new Analysis(edges, hadErrors, nestedIncomplete); 1970 } 1971 1972 private static boolean hasIncompleteNestedDynamicSql(DataFlowAnalyzer analyzer, 1973 DynamicSqlTrustMode trustMode) { 1974 if (trustMode == DynamicSqlTrustMode.LEGACY) { 1975 return false; 1976 } 1977 for (DynamicSqlSite site : analyzer.getDynamicSqlSites()) { 1978 // A nested execution is part of the outer program. Surface its 1979 // incompleteness in SHADOW without changing the published model. 1980 if (site.getStatus() != DynamicSqlSite.Status.RESOLVED) { 1981 return true; 1982 } 1983 } 1984 return false; 1985 } 1986 1987 /** A resolved object name never legitimately contains '@'; if it does, it still holds an unbound placeholder. */ 1988 private static boolean referencesPlaceholder(String name) { 1989 return name != null && name.indexOf('@') >= 0; 1990 } 1991 1992 /* ------------------------------------------------------------- helpers */ 1993 1994 private static void indexTables(Map<String, table> index, List<table> tables) { 1995 if (tables == null) { 1996 return; 1997 } 1998 for (table t : tables) { 1999 if (t != null && t.getId() != null) { 2000 index.put(t.getId(), t); 2001 } 2002 } 2003 } 2004 2005 private static String fullName(targetColumn c, Map<String, table> realTables, String currentDatabase, 2006 String defaultSchema, boolean showImplicit) { 2007 if (c == null || "system".equals(c.getSource())) { 2008 return null; 2009 } 2010 return joinName(c.getParent_id(), c.getParent_name(), c.getColumn(), realTables, currentDatabase, 2011 defaultSchema, showImplicit); 2012 } 2013 2014 private static String fullName(sourceColumn c, Map<String, table> realTables, String currentDatabase, 2015 String defaultSchema, boolean showImplicit) { 2016 return joinName(c.getParent_id(), c.getParent_name(), c.getColumn(), realTables, currentDatabase, 2017 defaultSchema, showImplicit); 2018 } 2019 2020 private static boolean isSystem(sourceColumn c) { 2021 return c == null || "system".equals(c.getSource()) || "RelationRows".equals(c.getColumn()); 2022 } 2023 2024 private static String joinName(String parentId, String parentName, String column, 2025 Map<String, table> realTables, String currentDatabase, String defaultSchema, boolean showImplicit) { 2026 if (column == null) { 2027 return null; 2028 } 2029 return qualifiedParent(parentId, parentName, realTables, currentDatabase, defaultSchema, showImplicit) 2030 + "." + stripBrackets(column); 2031 } 2032 2033 /** 2034 * Build the qualified parent name from the real-table model. dlineage may 2035 * return the name already qualified (db.schema.table), partly qualified 2036 * (schema.table), bare (table), or with an empty schema slot (db..table). We 2037 * normalize per-segment brackets positionally — last = table, then schema, 2038 * then database — and, only when {@code showImplicit} is on, fill a missing 2039 * schema/database from the proc's {@code defaultSchema}/{@code currentDatabase} 2040 * so an unqualified {@code RECON_JE_02_QSP_…} binds to 2041 * {@code JOURNALENGINE.dbo.RECON_JE_02_QSP_…}. Intermediate result sets 2042 * (no model table) keep their display name unchanged. 2043 */ 2044 private static String qualifiedParent(String parentId, String parentName, Map<String, table> realTables, 2045 String currentDatabase, String defaultSchema, boolean showImplicit) { 2046 table t = parentId == null ? null : realTables.get(parentId); 2047 if (t == null) { 2048 return stripBrackets(parentName); // result set / function / intermediate node 2049 } 2050 String full = t.getFullName() != null ? t.getFullName() : t.getName(); 2051 if (full == null || full.isEmpty()) { 2052 full = parentName; 2053 } 2054 if (full == null) { 2055 return stripBrackets(parentName); 2056 } 2057 2058 // Keep empty segments so positions are preserved (e.g. db..table). 2059 String[] raw = full.split("\\.", -1); 2060 for (int i = 0; i < raw.length; i++) { 2061 raw[i] = stripBrackets(raw[i].trim()); 2062 } 2063 int n = raw.length; 2064 String table = n >= 1 ? raw[n - 1] : stripBrackets(parentName); 2065 String schema = n >= 2 ? raw[n - 2] : ""; 2066 String db = n >= 3 ? raw[n - 3] : ""; 2067 // Anything left of the database segment (e.g. linked-server) is kept as a prefix. 2068 StringBuilder prefix = new StringBuilder(); 2069 for (int i = 0; i < n - 3; i++) { 2070 if (!raw[i].isEmpty()) { 2071 prefix.append(raw[i]).append('.'); 2072 } 2073 } 2074 if (showImplicit) { 2075 if (schema.isEmpty()) { 2076 schema = stripBrackets(defaultSchema); 2077 } 2078 if (db.isEmpty()) { 2079 db = stripBrackets(currentDatabase); 2080 } 2081 } 2082 StringBuilder sb = new StringBuilder(prefix.toString()); 2083 if (db != null && !db.isEmpty()) { 2084 sb.append(db).append('.'); 2085 } 2086 if (schema != null && !schema.isEmpty()) { 2087 sb.append(schema).append('.'); 2088 } 2089 sb.append(table == null ? "" : table); 2090 return sb.length() == 0 ? stripBrackets(parentName) : sb.toString(); 2091 } 2092 2093 private static TStatementList extractBody(TCustomSqlStatement procAst) { 2094 if (procAst instanceof TMssqlCreateProcedure) { 2095 return ((TMssqlCreateProcedure) procAst).getBodyStatements(); 2096 } 2097 return null; 2098 } 2099 2100 private static List<TParameterDeclaration> extractParams(TCustomSqlStatement procAst) { 2101 List<TParameterDeclaration> out = new ArrayList<TParameterDeclaration>(); 2102 if (procAst instanceof TMssqlCreateProcedure) { 2103 TParameterDeclarationList list = ((TMssqlCreateProcedure) procAst).getParameterDeclarations(); 2104 if (list != null) { 2105 for (int i = 0; i < list.size(); i++) { 2106 out.add(list.getParameterDeclarationItem(i)); 2107 } 2108 } 2109 } 2110 return out; 2111 } 2112 2113 private static String extractProcName(TCustomSqlStatement procAst) { 2114 if (procAst instanceof TMssqlCreateProcedure) { 2115 TMssqlCreateProcedure cp = (TMssqlCreateProcedure) procAst; 2116 if (cp.getProcedureName() != null) { 2117 return cp.getProcedureName().toString(); 2118 } 2119 } 2120 return procAst == null ? "" : procAst.getClass().getSimpleName(); 2121 } 2122 2123 private static long[] coordinate(TCustomSqlStatement node) { 2124 try { 2125 TSourceToken t = node.getStartToken(); 2126 if (t != null) { 2127 return new long[] { t.lineNo, t.columnNo }; 2128 } 2129 } catch (Throwable ignore) { 2130 // fall through 2131 } 2132 return new long[] { 0L, 0L }; 2133 } 2134 2135 /** Stable id robust to reformatting: proc identity + structural ordinal path, hashed. */ 2136 private static String dynamicSiteId(String sourceProc, String ordinalPath) { 2137 return "dyn:" + sha1Hex(stripBrackets(sourceProc) + "#" + ordinalPath).substring(0, 12); 2138 } 2139 2140 private static String hashBindings(Map<String, SqlValue> bindings) { 2141 if (bindings == null || bindings.isEmpty()) { 2142 return sha1Hex(""); 2143 } 2144 TreeMap<String, String> sorted = new TreeMap<String, String>(); 2145 for (Map.Entry<String, SqlValue> e : bindings.entrySet()) { 2146 if (e.getKey() == null) { 2147 continue; 2148 } 2149 sorted.put(stripBrackets(e.getKey().trim()).toLowerCase(), 2150 e.getValue() == null ? "NULL" : e.getValue().toString()); 2151 } 2152 StringBuilder sb = new StringBuilder(); 2153 for (Map.Entry<String, String> e : sorted.entrySet()) { 2154 sb.append(e.getKey()).append('=').append(e.getValue()).append(';'); 2155 } 2156 return sha1Hex(sb.toString()); 2157 } 2158 2159 private static String stripBrackets(String s) { 2160 if (s == null) { 2161 return ""; 2162 } 2163 s = s.trim(); 2164 if (s.length() >= 2) { 2165 char a = s.charAt(0); 2166 char z = s.charAt(s.length() - 1); 2167 if ((a == '[' && z == ']') || (a == '"' && z == '"') || (a == '`' && z == '`')) { 2168 return s.substring(1, s.length() - 1); 2169 } 2170 } 2171 return s; 2172 } 2173 2174 private static String sha1Hex(String input) { 2175 try { 2176 MessageDigest md = MessageDigest.getInstance("SHA-1"); 2177 byte[] digest = md.digest(input.getBytes("UTF-8")); 2178 StringBuilder sb = new StringBuilder(digest.length * 2); 2179 for (byte b : digest) { 2180 sb.append(Character.forDigit((b >> 4) & 0xF, 16)); 2181 sb.append(Character.forDigit(b & 0xF, 16)); 2182 } 2183 return sb.toString(); 2184 } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { 2185 return Integer.toHexString(input.hashCode()); 2186 } 2187 } 2188}