001package gudusoft.gsqlparser.dlineage; 002 003import gudusoft.gsqlparser.*; 004import gudusoft.gsqlparser.dlineage.dataflow.model.*; 005import gudusoft.gsqlparser.dlineage.util.DlineageUtil; 006import gudusoft.gsqlparser.nodes.*; 007import gudusoft.gsqlparser.stmt.*; 008import gudusoft.gsqlparser.stmt.oracle.*; 009import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 010import gudusoft.gsqlparser.sqlenv.QualifiedNameHierarchy; 011import gudusoft.gsqlparser.util.Logger; 012import gudusoft.gsqlparser.util.LoggerFactory; 013import gudusoft.gsqlparser.util.SQLUtil; 014 015import java.util.*; 016 017/** 018 * Handles Oracle pipelined table function cross-boundary data lineage stitching. 019 * 020 * <p>Two-phase approach: 021 * <ol> 022 * <li>Index phase: indexes object types, collection types, and pipelined function signatures</li> 023 * <li>Stitch phase: at call sites, appends lineage from function body to caller columns</li> 024 * </ol> 025 */ 026public class PipelinedFunctionAnalyzer { 027 028 private static final Logger logger = LoggerFactory.getLogger(PipelinedFunctionAnalyzer.class); 029 030 private final ModelBindingManager modelManager; 031 private final ModelFactory modelFactory; 032 private final Option option; 033 034 public PipelinedFunctionAnalyzer(ModelBindingManager modelManager, ModelFactory modelFactory, Option option) { 035 this.modelManager = modelManager; 036 this.modelFactory = modelFactory; 037 this.option = option; 038 } 039 040 // ---- Phase 1: Index CREATE TYPE AS OBJECT ---- 041 042 public void indexObjectType(TPlsqlCreateType stmt) { 043 if (!option.isEnablePipelinedStitching()) return; 044 if (stmt.getTypeAttributes() == null || stmt.getTypeAttributes().size() == 0) return; 045 if (stmt.getTypeName() == null) return; 046 047 String typeName = stmt.getTypeName().toString().trim(); 048 List<String> fieldNames = new ArrayList<>(); 049 for (int i = 0; i < stmt.getTypeAttributes().size(); i++) { 050 TTypeAttribute attr = stmt.getTypeAttributes().getAttributeItem(i); 051 if (attr.getAttributeName() != null) { 052 fieldNames.add(attr.getAttributeName().toString()); 053 } 054 } 055 modelManager.indexObjectTypeFields(typeKey(typeName), fieldNames); 056 // Also index by bare name (no schema prefix) 057 String bareName = getBareName(typeName); 058 if (!bareName.equals(typeName)) { 059 modelManager.indexObjectTypeFields(typeKey(bareName), fieldNames); 060 } 061 logger.trace("Indexed object type: " + typeName + " (bare=" + bareName + "), fields=" + fieldNames); 062 } 063 064 // ---- Phase 1: Index TABLE OF collection types ---- 065 066 public void indexCollectionType(TPlsqlTableTypeDefStmt stmt) { 067 if (!option.isEnablePipelinedStitching()) return; 068 if (stmt.getTypeName() == null || stmt.getElementDataType() == null) return; 069 070 String collectionName = stmt.getTypeName().toString().trim(); 071 String elementTypeName = stmt.getElementDataType().toString().trim(); 072 modelManager.indexCollectionElementType(typeKey(collectionName), elementTypeName); 073 // Also index by bare name 074 String bareCollection = getBareName(collectionName); 075 if (!bareCollection.equals(collectionName)) { 076 modelManager.indexCollectionElementType(typeKey(bareCollection), elementTypeName); 077 } 078 logger.trace("Indexed collection type: " + collectionName + " -> " + elementTypeName); 079 } 080 081 // ---- Phase 2: Detect pipelined function and build signature ---- 082 083 public void analyzePipelinedFunction(TPlsqlCreateFunction func) { 084 if (!option.isEnablePipelinedStitching()) return; 085 086 // Scan body for PIPE ROW statements using iterative traversal 087 boolean hasPipeRow = false; 088 List<TPlsqlPipeRowStmt> pipeRowStmts = new ArrayList<>(); 089 List<TAssignStmt> assignStmts = new ArrayList<>(); 090 List<TLoopStmt> cursorLoops = new ArrayList<>(); 091 092 Deque<TCustomSqlStatement> stack = new ArrayDeque<>(); 093 // Push body statements 094 for (int i = func.getBodyStatements().size() - 1; i >= 0; i--) { 095 stack.push(func.getBodyStatements().get(i)); 096 } 097 // Also check inner statements 098 for (int i = func.getStatements().size() - 1; i >= 0; i--) { 099 stack.push(func.getStatements().get(i)); 100 } 101 102 while (!stack.isEmpty()) { 103 TCustomSqlStatement s = stack.pop(); 104 if (s instanceof TPlsqlPipeRowStmt) { 105 pipeRowStmts.add((TPlsqlPipeRowStmt) s); 106 hasPipeRow = true; 107 } else if (s instanceof TAssignStmt) { 108 assignStmts.add((TAssignStmt) s); 109 } else if (s instanceof TLoopStmt) { 110 TLoopStmt loop = (TLoopStmt) s; 111 if (loop.getSubquery() != null || loop.getCursorName() != null) { 112 cursorLoops.add(loop); 113 } 114 // Push loop body statements 115 for (int i = loop.getBodyStatements().size() - 1; i >= 0; i--) { 116 stack.push(loop.getBodyStatements().get(i)); 117 } 118 } else if (s instanceof TIfStmt) { 119 TIfStmt ifStmt = (TIfStmt) s; 120 if (ifStmt.getThenStatements() != null) { 121 for (int i = ifStmt.getThenStatements().size() - 1; i >= 0; i--) { 122 stack.push(ifStmt.getThenStatements().get(i)); 123 } 124 } 125 if (ifStmt.getElseStatements() != null) { 126 for (int i = ifStmt.getElseStatements().size() - 1; i >= 0; i--) { 127 stack.push(ifStmt.getElseStatements().get(i)); 128 } 129 } 130 if (ifStmt.getElseifStatements() != null) { 131 for (int i = ifStmt.getElseifStatements().size() - 1; i >= 0; i--) { 132 stack.push(ifStmt.getElseifStatements().get(i)); 133 } 134 } 135 } else if (s instanceof TCommonBlock) { 136 TCommonBlock block = (TCommonBlock) s; 137 if (block.getBodyStatements() != null) { 138 for (int i = block.getBodyStatements().size() - 1; i >= 0; i--) { 139 stack.push(block.getBodyStatements().get(i)); 140 } 141 } 142 if (block.getStatements() != null) { 143 for (int i = block.getStatements().size() - 1; i >= 0; i--) { 144 stack.push(block.getStatements().get(i)); 145 } 146 } 147 } 148 // Fallback: try to push sub-statements for any other compound statement type 149 else { 150 if (s.getStatements() != null && s.getStatements().size() > 0) { 151 for (int i = s.getStatements().size() - 1; i >= 0; i--) { 152 stack.push(s.getStatements().get(i)); 153 } 154 } 155 if (s instanceof TBlockSqlStatement) { 156 TBlockSqlStatement blockStmt = (TBlockSqlStatement) s; 157 if (blockStmt.getBodyStatements() != null && blockStmt.getBodyStatements().size() > 0) { 158 for (int i = blockStmt.getBodyStatements().size() - 1; i >= 0; i--) { 159 stack.push(blockStmt.getBodyStatements().get(i)); 160 } 161 } 162 } 163 } 164 } 165 166 if (!hasPipeRow) return; 167 168 // Build signature 169 PipelinedFunctionSignature sig = new PipelinedFunctionSignature(); 170 String funcName = func.getFunctionName() != null ? func.getFunctionName().toString() : ""; 171 172 // Build function key with package context 173 String packagePrefix = ""; 174 if (ModelBindingManager.getGlobalOraclePackage() != null) { 175 packagePrefix = ModelBindingManager.getGlobalOraclePackage().getName(); 176 if (packagePrefix != null && !packagePrefix.isEmpty()) { 177 packagePrefix = packagePrefix + "."; 178 } else { 179 packagePrefix = ""; 180 } 181 } 182 183 int argCount = func.getParameterDeclarations() != null ? func.getParameterDeclarations().size() : 0; 184 185 String fullName = packagePrefix + funcName; 186 String nakedName = getBareName(funcName); 187 188 sig.setFunctionKey(fullName); 189 sig.setNakedKey(nakedName); 190 sig.setFunctionIdentity(routineKey(fullName)); 191 sig.setNakedIdentity(routineKey(nakedName)); 192 sig.setArgCount(argCount); 193 194 // Resolve return type → collection type → element/object type → output columns 195 TTypeName returnType = func.getReturnDataType(); 196 logger.trace("Pipelined func " + funcName + ": returnType=" + (returnType != null ? returnType.toString() : "null")); 197 if (returnType != null) { 198 String returnTypeName = returnType.toString().trim(); 199 logger.trace(" returnTypeName=" + returnTypeName); 200 // Try as collection type first 201 String elementType = modelManager.getCollectionElementType(typeKey(returnTypeName)); 202 if (elementType == null) { 203 elementType = modelManager.getCollectionElementType(typeKey(getBareName(returnTypeName))); 204 } 205 if (elementType != null) { 206 sig.setCollectionTypeKey(returnTypeName); 207 sig.setRowTypeKey(elementType); 208 } else { 209 // Maybe the return type IS the object type directly 210 sig.setRowTypeKey(returnTypeName); 211 } 212 213 // Resolve output columns from object type index 214 String rowTypeKey = sig.getRowTypeKey(); 215 List<String> fields = null; 216 if (rowTypeKey != null) { 217 fields = modelManager.getObjectTypeFieldsByKey(typeKey(rowTypeKey)); 218 if (fields == null) { 219 fields = modelManager.getObjectTypeFieldsByKey(typeKey(getBareName(rowTypeKey))); 220 } 221 } 222 if (fields != null) { 223 sig.setOutputColumns(new ArrayList<>(fields)); 224 } 225 } 226 227 // Phase 3: Extract PIPE ROW lineage 228 if (sig.getOutputColumns() != null && !sig.getOutputColumns().isEmpty()) { 229 resolvePipeRowLineage(func, sig, pipeRowStmts, assignStmts, cursorLoops); 230 } else { 231 sig.setStatus(PipelinedFunctionSignature.ResolutionStatus.UNRESOLVED); 232 } 233 234 modelManager.addPipelinedSignature(sig); 235 logger.trace("Pipelined signature built: key=" + sig.getFunctionKey() 236 + ", nakedKey=" + sig.getNakedKey() 237 + ", resolved=" + sig.isResolved() 238 + ", status=" + sig.getStatus() 239 + ", outputCols=" + sig.getOutputColumns() 240 + ", lineageKeys=" + sig.getLineageByOutputColumn().keySet() 241 + ", lineageEntries=" + sig.getLineageByOutputColumn().size()); 242 for (Map.Entry<String, List<PipelinedSourceRef>> entry : sig.getLineageByOutputColumn().entrySet()) { 243 logger.trace(" " + entry.getKey() + " <- " + entry.getValue()); 244 } 245 } 246 247 // ---- Phase 3: PIPE ROW lineage extraction ---- 248 249 private void resolvePipeRowLineage(TPlsqlCreateFunction func, PipelinedFunctionSignature sig, 250 List<TPlsqlPipeRowStmt> pipeRowStmts, 251 List<TAssignStmt> assignStmts, 252 List<TLoopStmt> cursorLoops) { 253 // Build cursor bindings from explicit CURSOR declarations 254 // Check both declare section (getStatements) and body (getBodyStatements) 255 Map<String, TSelectSqlStatement> cursorBindings = new LinkedHashMap<>(); 256 for (int i = 0; i < func.getStatements().size(); i++) { 257 TCustomSqlStatement s = func.getStatements().get(i); 258 if (s instanceof TCursorDeclStmt) { 259 TCursorDeclStmt cursorDecl = (TCursorDeclStmt) s; 260 if (cursorDecl.getCursorName() != null && cursorDecl.getSubquery() != null) { 261 cursorBindings.put(cursorDecl.getCursorName().toString().toLowerCase(Locale.ROOT), cursorDecl.getSubquery()); 262 } 263 } 264 } 265 for (int i = 0; i < func.getBodyStatements().size(); i++) { 266 TCustomSqlStatement s = func.getBodyStatements().get(i); 267 if (s instanceof TCursorDeclStmt) { 268 TCursorDeclStmt cursorDecl = (TCursorDeclStmt) s; 269 if (cursorDecl.getCursorName() != null && cursorDecl.getSubquery() != null) { 270 cursorBindings.put(cursorDecl.getCursorName().toString().toLowerCase(Locale.ROOT), cursorDecl.getSubquery()); 271 } 272 } 273 } 274 275 // Build loop variable bindings: loopVar -> cursor SELECT 276 Map<String, TSelectSqlStatement> loopVarBindings = new LinkedHashMap<>(); 277 for (TLoopStmt loop : cursorLoops) { 278 String loopVar = null; 279 if (loop.getRecordName() != null) { 280 loopVar = loop.getRecordName().toString().toLowerCase(Locale.ROOT); 281 } else if (loop.getIndexName() != null) { 282 loopVar = loop.getIndexName().toString().toLowerCase(Locale.ROOT); 283 } 284 if (loopVar == null) continue; 285 286 if (loop.getSubquery() != null) { 287 // Inline SELECT: FOR item IN (SELECT ...) LOOP 288 loopVarBindings.put(loopVar, loop.getSubquery()); 289 } else if (loop.getCursorName() != null) { 290 // Named cursor: FOR item IN cursorName LOOP 291 String cursorName = loop.getCursorName().toString().toLowerCase(Locale.ROOT); 292 TSelectSqlStatement cursorSelect = cursorBindings.get(cursorName); 293 if (cursorSelect != null) { 294 loopVarBindings.put(loopVar, cursorSelect); 295 } 296 logger.trace("Named cursor loop: var=" + loopVar + ", cursor=" + cursorName 297 + ", resolved=" + (cursorSelect != null)); 298 } 299 } 300 301 // Build assignment maps 302 // outRow := ROW_TYPE(...) --> variable -> list of constructor expressions (union from multiple branches) 303 Map<String, List<TExpression>> varConstructors = new LinkedHashMap<>(); 304 // outRow.field := expr --> variable -> field -> expression 305 Map<String, Map<String, TExpression>> varFieldAssignments = new LinkedHashMap<>(); 306 307 for (TAssignStmt assign : assignStmts) { 308 TExpression leftExpr = assign.getLeft(); 309 TExpression rightExpr = assign.getExpression(); 310 if (leftExpr == null || rightExpr == null) continue; 311 312 String leftText = leftExpr.toString().trim(); 313 int dotIdx = leftText.indexOf('.'); 314 if (dotIdx > 0) { 315 // outRow.field := expr 316 String varName = leftText.substring(0, dotIdx).toLowerCase(Locale.ROOT); 317 String fieldName = leftText.substring(dotIdx + 1); 318 varFieldAssignments.computeIfAbsent(varName, k -> new LinkedHashMap<>()) 319 .put(fieldName, rightExpr); 320 } else { 321 // outRow := ROW_TYPE(...) 322 String varName = leftText.toLowerCase(Locale.ROOT); 323 varConstructors.computeIfAbsent(varName, k -> new ArrayList<>()) 324 .add(rightExpr); 325 } 326 } 327 328 // Check for cursor FOR loops using named cursors (FOR item IN cursorName LOOP) 329 for (TLoopStmt loop : cursorLoops) { 330 if (loop.getIndexName() != null && loop.getSubquery() == null) { 331 // Might reference a named cursor 332 String loopVar = loop.getIndexName().toString().toLowerCase(Locale.ROOT); 333 // Look for cursor name in the loop definition 334 // The cursor name would be in the loop's cursor expression 335 // This is handled differently - the subquery is already resolved 336 } 337 } 338 339 // Process each PIPE ROW statement 340 for (TPlsqlPipeRowStmt pipeRow : pipeRowStmts) { 341 TExpression expr = pipeRow.getExpression(); 342 if (expr == null) continue; 343 344 String exprText = expr.toString().trim(); 345 346 if (isObjectConstructor(expr)) { 347 // PIPE ROW(ROW_TYPE(arg1, arg2, ...)) 348 bindConstructorArgs(sig, expr, loopVarBindings, cursorBindings); 349 } else { 350 // PIPE ROW(outRow) - variable reference 351 String varName = exprText.toLowerCase(Locale.ROOT); 352 353 // Check for constructor assignments: outRow := ROW_TYPE(...) 354 List<TExpression> constructors = varConstructors.get(varName); 355 if (constructors != null) { 356 for (TExpression constructor : constructors) { 357 if (isObjectConstructor(constructor)) { 358 bindConstructorArgs(sig, constructor, loopVarBindings, cursorBindings); 359 } 360 } 361 } 362 363 // Check for field assignments: outRow.field := expr 364 Map<String, TExpression> fieldAssigns = varFieldAssignments.get(varName); 365 if (fieldAssigns != null) { 366 overlayFieldAssignments(sig, fieldAssigns, loopVarBindings, cursorBindings); 367 } 368 } 369 } 370 371 sig.setResolved(true); 372 sig.setStatus(PipelinedFunctionSignature.ResolutionStatus.OK); 373 } 374 375 private boolean isObjectConstructor(TExpression expr) { 376 if (expr == null) return false; 377 // A constructor call looks like a function call: ROW_TYPE(arg1, arg2, ...) 378 if (expr.getExpressionType() == EExpressionType.function_t && expr.getFunctionCall() != null) { 379 return true; 380 } 381 // Also handle simple_object_expr_t 382 if (expr.getExpressionType() == EExpressionType.simple_object_name_t) { 383 return false; 384 } 385 // Check if it's a function call expression 386 String text = expr.toString().trim(); 387 if (text.contains("(") && text.endsWith(")")) { 388 return expr.getFunctionCall() != null; 389 } 390 return false; 391 } 392 393 private void bindConstructorArgs(PipelinedFunctionSignature sig, TExpression constructorExpr, 394 Map<String, TSelectSqlStatement> loopVarBindings, 395 Map<String, TSelectSqlStatement> cursorBindings) { 396 TFunctionCall funcCall = constructorExpr.getFunctionCall(); 397 if (funcCall == null || funcCall.getArgs() == null) return; 398 399 TExpressionList args = funcCall.getArgs(); 400 List<String> outputColumns = sig.getOutputColumns(); 401 402 for (int i = 0; i < args.size() && i < outputColumns.size(); i++) { 403 TExpression arg = args.getExpression(i); 404 String outputCol = outputColumns.get(i).toLowerCase(Locale.ROOT); 405 resolveArgLineage(sig, outputCol, arg, loopVarBindings, cursorBindings); 406 } 407 } 408 409 private void resolveArgLineage(PipelinedFunctionSignature sig, String outputCol, 410 TExpression arg, 411 Map<String, TSelectSqlStatement> loopVarBindings, 412 Map<String, TSelectSqlStatement> cursorBindings) { 413 if (arg == null) return; 414 415 String argText = arg.toString().trim(); 416 417 // Check if it's a wrapper function like util.TO_NUMBER(item.xxx) - extract inner argument 418 TExpression innerArg = unwrapFunctionCalls(arg); 419 if (innerArg != arg) { 420 argText = innerArg.toString().trim(); 421 } 422 423 // Check for loop variable reference: item.COLUMN_NAME 424 int dotIdx = argText.indexOf('.'); 425 if (dotIdx > 0) { 426 String prefix = argText.substring(0, dotIdx).toLowerCase(Locale.ROOT); 427 String fieldName = argText.substring(dotIdx + 1); 428 429 // Check loop variable bindings 430 TSelectSqlStatement cursorSelect = loopVarBindings.get(prefix); 431 if (cursorSelect == null) { 432 cursorSelect = cursorBindings.get(prefix); 433 } 434 if (cursorSelect != null) { 435 // Resolve the field from the cursor's select list 436 resolveFromCursorSelect(sig, outputCol, fieldName, cursorSelect); 437 return; 438 } 439 } 440 441 // Check for simple column reference (no prefix) 442 if (dotIdx < 0 && !argText.contains("(")) { 443 // Could be a local variable or parameter - mark as unresolved for now 444 sig.addSourceRef(outputCol, new PipelinedSourceRef( 445 "unknown", argText, PipelinedSourceRef.SourceKind.UNRESOLVED)); 446 return; 447 } 448 449 // Check for literal/constant 450 if (argText.equals("null") || argText.startsWith("'") || argText.matches("-?\\d+.*")) { 451 // Constant - empty source, just preserve column structure 452 return; 453 } 454 455 // For qualified names like table.column that aren't loop variables 456 if (dotIdx > 0) { 457 String tablePart = argText.substring(0, dotIdx); 458 String colPart = argText.substring(dotIdx + 1); 459 sig.addSourceRef(outputCol, new PipelinedSourceRef( 460 tablePart, colPart, PipelinedSourceRef.SourceKind.BASE_TABLE)); 461 } 462 } 463 464 private TExpression unwrapFunctionCalls(TExpression expr) { 465 // Unwrap wrapper functions like TO_NUMBER(x), TO_CHAR(x), TRIM(x), util.TO_NUMBER(x) 466 if (expr.getExpressionType() == EExpressionType.function_t && expr.getFunctionCall() != null) { 467 TFunctionCall func = expr.getFunctionCall(); 468 if (func.getArgs() != null && func.getArgs().size() == 1) { 469 String funcName = func.getFunctionName().toString().toUpperCase(Locale.ROOT); 470 // Common wrapper functions 471 if (funcName.endsWith("TO_NUMBER") || funcName.endsWith("TO_CHAR") 472 || funcName.endsWith("TO_DATE") || funcName.equals("TRIM") 473 || funcName.equals("NVL") || funcName.equals("COALESCE") 474 || funcName.equals("UPPER") || funcName.equals("LOWER") 475 || funcName.equals("CAST") || funcName.endsWith(".TO_NUMBER")) { 476 return unwrapFunctionCalls(func.getArgs().getExpression(0)); 477 } 478 } 479 } 480 return expr; 481 } 482 483 private void resolveFromCursorSelect(PipelinedFunctionSignature sig, String outputCol, 484 String fieldName, TSelectSqlStatement cursorSelect) { 485 // Find the column in the cursor's SELECT list that matches fieldName 486 if (cursorSelect.getResultColumnList() == null) return; 487 488 String fieldNameLower = fieldName.toLowerCase(Locale.ROOT); 489 // Strip quotes 490 fieldNameLower = SQLUtil.trimColumnStringQuote(fieldNameLower); 491 492 for (int i = 0; i < cursorSelect.getResultColumnList().size(); i++) { 493 TResultColumn rc = cursorSelect.getResultColumnList().getResultColumn(i); 494 String alias = null; 495 if (rc.getAliasClause() != null) { 496 alias = rc.getAliasClause().toString().toLowerCase(Locale.ROOT); 497 alias = SQLUtil.trimColumnStringQuote(alias); 498 } 499 String colName = null; 500 if (rc.getExpr() != null) { 501 colName = rc.getExpr().toString().toLowerCase(Locale.ROOT); 502 // Extract just the column part from table.column 503 int dot = colName.lastIndexOf('.'); 504 if (dot >= 0) { 505 colName = colName.substring(dot + 1); 506 } 507 colName = SQLUtil.trimColumnStringQuote(colName); 508 } 509 510 if (fieldNameLower.equals(alias) || fieldNameLower.equals(colName)) { 511 // Found the matching column - trace its source 512 resolveResultColumnSources(sig, outputCol, rc, cursorSelect); 513 return; 514 } 515 } 516 517 // Didn't find by name, try position if same number of columns 518 sig.addSourceRef(outputCol, new PipelinedSourceRef( 519 "cursor", fieldName, PipelinedSourceRef.SourceKind.UNRESOLVED)); 520 } 521 522 private void resolveResultColumnSources(PipelinedFunctionSignature sig, String outputCol, 523 TResultColumn rc, TSelectSqlStatement select) { 524 resolveResultColumnSourcesWithDepth(sig, outputCol, rc, select, 0); 525 } 526 527 private static final int MAX_CTE_TRACE_DEPTH = 10; 528 529 private void resolveResultColumnSourcesWithDepth(PipelinedFunctionSignature sig, String outputCol, 530 TResultColumn rc, TSelectSqlStatement select, int depth) { 531 if (rc.getExpr() == null) return; 532 if (depth > MAX_CTE_TRACE_DEPTH) return; 533 534 TExpression expr = rc.getExpr(); 535 // Unwrap wrapper functions 536 expr = unwrapFunctionCalls(expr); 537 String exprText = expr.toString().trim(); 538 539 // Try to resolve to a base table column 540 int dotIdx = exprText.lastIndexOf('.'); 541 if (dotIdx > 0) { 542 String tableRef = exprText.substring(0, dotIdx); 543 String colRef = exprText.substring(dotIdx + 1); 544 545 // Check if the table reference points to a CTE - if so, trace through it 546 TTable sourceTable = findTableByRefInSelect(tableRef, select); 547 if (sourceTable != null && sourceTable.isCTEName()) { 548 // Use the actual table name (not the alias) to find the CTE definition 549 String cteName = getFullTableName(sourceTable); 550 if (traceThroughCTE(sig, outputCol, colRef, cteName, select, depth)) { 551 return; 552 } 553 } 554 555 // Resolve table reference against FROM clause 556 String resolvedTable = resolveTableRef(tableRef, select); 557 if (resolvedTable != null) { 558 sig.addSourceRef(outputCol, new PipelinedSourceRef( 559 resolvedTable, SQLUtil.trimColumnStringQuote(colRef), 560 PipelinedSourceRef.SourceKind.BASE_TABLE)); 561 return; 562 } 563 // Fallback: use the table ref as-is 564 sig.addSourceRef(outputCol, new PipelinedSourceRef( 565 tableRef, SQLUtil.trimColumnStringQuote(colRef), 566 PipelinedSourceRef.SourceKind.BASE_TABLE)); 567 } else if (exprText.equals("null") || exprText.startsWith("'") || exprText.matches("-?\\d+.*")) { 568 // Constant - just preserve column structure 569 sig.addSourceRef(outputCol, new PipelinedSourceRef( 570 "constant", exprText, PipelinedSourceRef.SourceKind.CONST)); 571 } else if (!exprText.contains("(") && !exprText.contains(" ")) { 572 // Simple column name without table prefix 573 // Try to resolve by checking sourceTable from the parser's column resolution 574 if (rc.getExpr().getObjectOperand() != null 575 && rc.getExpr().getObjectOperand().getSourceTable() != null) { 576 TTable sourceTable = rc.getExpr().getObjectOperand().getSourceTable(); 577 // If the source table is a CTE, trace through it to find actual base tables 578 if (sourceTable.isCTEName()) { 579 String cteName = getFullTableName(sourceTable); 580 if (traceThroughCTE(sig, outputCol, exprText, cteName, select, depth)) { 581 return; 582 } 583 } 584 String tableName = getFullTableName(sourceTable); 585 sig.addSourceRef(outputCol, new PipelinedSourceRef( 586 tableName, SQLUtil.trimColumnStringQuote(exprText), 587 PipelinedSourceRef.SourceKind.BASE_TABLE)); 588 } else { 589 // Try to find which table it belongs to from the FROM clause 590 String resolvedTable = resolveColumnTable(exprText, select); 591 if (resolvedTable != null) { 592 sig.addSourceRef(outputCol, new PipelinedSourceRef( 593 resolvedTable, SQLUtil.trimColumnStringQuote(exprText), 594 PipelinedSourceRef.SourceKind.BASE_TABLE)); 595 } else { 596 // Fallback: use the SELECT itself as the source - the analyzer will trace further 597 // Use the cursor/select name as a proxy source 598 String selectName = "cursor_result"; 599 if (select.tables != null && select.tables.size() > 0) { 600 selectName = getFullTableName(select.tables.getTable(0)); 601 } 602 sig.addSourceRef(outputCol, new PipelinedSourceRef( 603 selectName, SQLUtil.trimColumnStringQuote(exprText), 604 PipelinedSourceRef.SourceKind.CTE)); 605 } 606 } 607 } else { 608 // Complex expression - try extracting column refs 609 extractColumnRefsFromExpr(sig, outputCol, expr, select); 610 } 611 } 612 613 /** 614 * Traces a column reference through a CTE to find actual base tables. 615 * Returns true if at least one base table source was found. 616 */ 617 private boolean traceThroughCTE(PipelinedFunctionSignature sig, String outputCol, 618 String colName, String cteRef, TSelectSqlStatement select, int depth) { 619 if (depth > MAX_CTE_TRACE_DEPTH) return false; 620 621 // Find the CTE definition matching cteRef 622 TCTE cte = findCTE(cteRef, select); 623 if (cte == null || cte.getSubquery() == null) return false; 624 625 TSelectSqlStatement cteSelect = cte.getSubquery(); 626 // Handle the outermost SELECT of the CTE - for UNION, use leftStmt chain 627 if (cteSelect.getSetOperatorType() != ESetOperatorType.none) { 628 // For UNION queries, trace through the first branch 629 Deque<TSelectSqlStatement> unionStack = new ArrayDeque<>(); 630 unionStack.push(cteSelect); 631 while (!unionStack.isEmpty()) { 632 TSelectSqlStatement current = unionStack.pop(); 633 if (current.getSetOperatorType() != ESetOperatorType.none) { 634 if (current.getRightStmt() != null) unionStack.push(current.getRightStmt()); 635 if (current.getLeftStmt() != null) unionStack.push(current.getLeftStmt()); 636 } else { 637 // Process this leaf SELECT 638 traceCTESelectColumn(sig, outputCol, colName, current, select, depth); 639 return true; // Just use the first branch for lineage 640 } 641 } 642 return false; 643 } 644 645 return traceCTESelectColumn(sig, outputCol, colName, cteSelect, select, depth); 646 } 647 648 /** 649 * Traces a column through a CTE's SELECT statement to find its source. 650 */ 651 private boolean traceCTESelectColumn(PipelinedFunctionSignature sig, String outputCol, 652 String colName, TSelectSqlStatement cteSelect, 653 TSelectSqlStatement outerSelect, int depth) { 654 if (cteSelect.getResultColumnList() == null) return false; 655 656 String colNameLower = SQLUtil.trimColumnStringQuote(colName.toLowerCase(Locale.ROOT)); 657 658 // Find the matching result column in the CTE's SELECT list 659 for (int i = 0; i < cteSelect.getResultColumnList().size(); i++) { 660 TResultColumn rc = cteSelect.getResultColumnList().getResultColumn(i); 661 String alias = null; 662 if (rc.getAliasClause() != null) { 663 alias = rc.getAliasClause().toString().toLowerCase(Locale.ROOT); 664 alias = SQLUtil.trimColumnStringQuote(alias); 665 } 666 String rcColName = null; 667 if (rc.getExpr() != null) { 668 rcColName = rc.getExpr().toString().toLowerCase(Locale.ROOT); 669 int dot = rcColName.lastIndexOf('.'); 670 if (dot >= 0) { 671 rcColName = rcColName.substring(dot + 1); 672 } 673 rcColName = SQLUtil.trimColumnStringQuote(rcColName); 674 } 675 676 if (colNameLower.equals(alias) || colNameLower.equals(rcColName)) { 677 // Found the column - recursively resolve its source 678 resolveResultColumnSourcesWithDepth(sig, outputCol, rc, cteSelect, depth + 1); 679 return true; 680 } 681 } 682 return false; 683 } 684 685 /** 686 * Finds a CTE by name in the given SELECT or its parent statements. 687 */ 688 private TCTE findCTE(String cteRef, TSelectSqlStatement select) { 689 String cteRefLower = SQLUtil.trimColumnStringQuote(cteRef.toLowerCase(Locale.ROOT)); 690 // Also try bare name 691 String cteRefBare = getBareName(cteRefLower); 692 693 TCustomSqlStatement current = select; 694 while (current != null) { 695 if (current instanceof TSelectSqlStatement) { 696 TSelectSqlStatement sel = (TSelectSqlStatement) current; 697 if (sel.getCteList() != null) { 698 for (int i = 0; i < sel.getCteList().size(); i++) { 699 TCTE cte = sel.getCteList().getCTE(i); 700 if (cte.getTableName() != null) { 701 String cteName = cte.getTableName().toString().toLowerCase(Locale.ROOT); 702 if (cteRefLower.equals(cteName) || cteRefBare.equals(cteName)) { 703 return cte; 704 } 705 } 706 } 707 } 708 } 709 current = current.getParentStmt(); 710 } 711 return null; 712 } 713 714 /** 715 * Finds a TTable in the SELECT's FROM clause by reference (name or alias). 716 */ 717 private TTable findTableByRefInSelect(String tableRef, TSelectSqlStatement select) { 718 if (select == null || select.tables == null) return null; 719 String refLower = SQLUtil.trimColumnStringQuote(tableRef.toLowerCase(Locale.ROOT)); 720 721 for (int i = 0; i < select.tables.size(); i++) { 722 TTable t = select.tables.getTable(i); 723 if (t.getAliasClause() != null) { 724 String alias = SQLUtil.trimColumnStringQuote(t.getAliasClause().toString().toLowerCase(Locale.ROOT)); 725 if (alias.equals(refLower)) return t; 726 } 727 String tName = t.getFullName() != null ? t.getFullName().toLowerCase(Locale.ROOT) : ""; 728 String bareTableName = getBareName(tName); 729 if (refLower.equals(bareTableName) || refLower.equals(tName)) return t; 730 } 731 return null; 732 } 733 734 private void extractColumnRefsFromExpr(PipelinedFunctionSignature sig, String outputCol, 735 TExpression expr, TSelectSqlStatement select) { 736 // Use iterative traversal to find object names in the expression 737 Deque<TExpression> exprStack = new ArrayDeque<>(); 738 exprStack.push(expr); 739 boolean found = false; 740 741 while (!exprStack.isEmpty()) { 742 TExpression current = exprStack.pop(); 743 if (current == null) continue; 744 745 if (current.getExpressionType() == EExpressionType.simple_object_name_t 746 && current.getObjectOperand() != null) { 747 String objName = current.getObjectOperand().toString(); 748 int dot = objName.lastIndexOf('.'); 749 if (dot > 0) { 750 String tableRef = objName.substring(0, dot); 751 String colRef = objName.substring(dot + 1); 752 String resolvedTable = resolveTableRef(tableRef, select); 753 sig.addSourceRef(outputCol, new PipelinedSourceRef( 754 resolvedTable != null ? resolvedTable : tableRef, 755 SQLUtil.trimColumnStringQuote(colRef), 756 PipelinedSourceRef.SourceKind.BASE_TABLE)); 757 found = true; 758 } 759 } 760 761 // Push children 762 if (current.getLeftOperand() != null) exprStack.push(current.getLeftOperand()); 763 if (current.getRightOperand() != null) exprStack.push(current.getRightOperand()); 764 if (current.getFunctionCall() != null && current.getFunctionCall().getArgs() != null) { 765 for (int i = 0; i < current.getFunctionCall().getArgs().size(); i++) { 766 exprStack.push(current.getFunctionCall().getArgs().getExpression(i)); 767 } 768 } 769 } 770 771 if (!found) { 772 sig.addSourceRef(outputCol, new PipelinedSourceRef( 773 "expression", expr.toString(), PipelinedSourceRef.SourceKind.UNRESOLVED)); 774 } 775 } 776 777 private String resolveTableRef(String tableRef, TSelectSqlStatement select) { 778 if (select == null || select.tables == null) return null; 779 String tableRefLower = tableRef.toLowerCase(Locale.ROOT); 780 String tableRefNorm = SQLUtil.trimColumnStringQuote(tableRefLower); 781 782 for (int i = 0; i < select.tables.size(); i++) { 783 TTable t = select.tables.getTable(i); 784 // Check alias 785 if (t.getAliasClause() != null) { 786 String alias = t.getAliasClause().toString().toLowerCase(Locale.ROOT); 787 alias = SQLUtil.trimColumnStringQuote(alias); 788 if (alias.equals(tableRefNorm)) { 789 return getFullTableName(t); 790 } 791 } 792 // Check table name 793 String tName = t.getFullName() != null ? t.getFullName().toLowerCase(Locale.ROOT) : ""; 794 String bareTableName = getBareName(tName); 795 if (tableRefNorm.equals(bareTableName) || tableRefNorm.equals(tName)) { 796 return getFullTableName(t); 797 } 798 } 799 800 // Check CTEs 801 if (select.getCteList() != null) { 802 for (int i = 0; i < select.getCteList().size(); i++) { 803 TCTE cte = select.getCteList().getCTE(i); 804 if (cte.getTableName() != null) { 805 String cteName = cte.getTableName().toString().toLowerCase(Locale.ROOT); 806 if (tableRefNorm.equals(cteName)) { 807 return cteName; 808 } 809 } 810 } 811 } 812 813 // If this is a subquery's select, walk up to find CTEs 814 TCustomSqlStatement parent = select.getParentStmt(); 815 if (parent instanceof TSelectSqlStatement) { 816 return resolveTableRef(tableRef, (TSelectSqlStatement) parent); 817 } 818 819 return null; 820 } 821 822 private String resolveColumnTable(String colName, TSelectSqlStatement select) { 823 if (select == null || select.tables == null) return null; 824 // Simplified: just return null to indicate we can't determine the table 825 return null; 826 } 827 828 private String getFullTableName(TTable table) { 829 if (table.getFullName() != null) { 830 return table.getFullName(); 831 } 832 return table.getName(); 833 } 834 835 private void overlayFieldAssignments(PipelinedFunctionSignature sig, 836 Map<String, TExpression> fieldAssigns, 837 Map<String, TSelectSqlStatement> loopVarBindings, 838 Map<String, TSelectSqlStatement> cursorBindings) { 839 for (Map.Entry<String, TExpression> entry : fieldAssigns.entrySet()) { 840 String fieldName = entry.getKey(); 841 TExpression expr = entry.getValue(); 842 843 // Find matching output column 844 String outputCol = null; 845 for (String col : sig.getOutputColumns()) { 846 if (SQLUtil.sameName(option.getVendor(), ESQLDataObjectType.dotColumn, col, fieldName)) { 847 outputCol = col.toLowerCase(Locale.ROOT); 848 break; 849 } 850 } 851 if (outputCol == null) continue; 852 853 resolveArgLineage(sig, outputCol, expr, loopVarBindings, cursorBindings); 854 } 855 } 856 857 // ---- Phase 4: Call-site stitching ---- 858 859 /** 860 * Handle TABLE(func(...)) which is parsed as tableExpr with funcCall=null. 861 * The table name is the function name. Always defers to finalize because 862 * table columns are not yet populated during from-clause processing. 863 */ 864 public void tryStitchTableExpr(TTable table) { 865 if (!option.isEnablePipelinedStitching()) return; 866 if (table.getName() == null) return; 867 868 String tableName = table.getName().trim(); 869 List<String> names = new ArrayList<>(); 870 names.add(tableName); 871 String bareName = getBareName(tableName); 872 if (!bareName.equals(tableName)) { 873 names.add(bareName); 874 } 875 List<Object> candidates = routineKeys(names); 876 877 // Always defer to finalize - columns aren't populated yet during FROM processing 878 modelManager.addPendingPipelinedCallSite( 879 PendingPipelinedCallSite.forFunctionKeys(names, candidates, -1, table, null)); 880 } 881 882 /** 883 * Called when TABLE(func(...)) is encountered in a FROM clause. 884 * Generates candidate keys and tries to stitch immediately. 885 * If the signature isn't available yet, adds to pending list. 886 */ 887 public boolean tryStitchCallSite(TTable table, TFunctionCall funcCall) { 888 if (!option.isEnablePipelinedStitching()) return false; 889 890 List<String> names = generateFunctionNameCandidates(funcCall); 891 List<Object> candidates = routineKeys(names); 892 int argCount = funcCall.getArgs() != null ? funcCall.getArgs().size() : 0; 893 894 PipelinedFunctionSignature sig = modelManager.findPipelinedSignatureByKeys(candidates, argCount); 895 if (sig == null) { 896 sig = modelManager.findPipelinedSignatureAnyByKeys(candidates); 897 } 898 899 if (sig != null && sig.isResolved() 900 && sig.getStatus() == PipelinedFunctionSignature.ResolutionStatus.OK) { 901 logger.trace("Pipelined stitch: found signature " + sig.getFunctionKey() 902 + " for call " + funcCall.getFunctionName() + ", cols=" + sig.getOutputColumns().size() 903 + ", lineage keys=" + sig.getLineageByOutputColumn().keySet()); 904 stitchOneCallSite(table, sig); 905 return true; 906 } 907 908 // Add to pending for finalize 909 logger.trace("Pipelined stitch: no signature found for candidates=" + candidates 910 + ", adding to pending"); 911 modelManager.addPendingPipelinedCallSite( 912 PendingPipelinedCallSite.forFunctionKeys(names, candidates, argCount, table, funcCall)); 913 return false; 914 } 915 916 /** 917 * Stitch a single pending call site. Called from DataFlowAnalyzer with 918 * the proper statement stack context. 919 */ 920 public void stitchOnePending(PendingPipelinedCallSite cs) { 921 if (!option.isEnablePipelinedStitching()) return; 922 923 PipelinedFunctionSignature sig = modelManager.findPipelinedSignatureByKeys( 924 cs.getFunctionKeys(), cs.getArgCount()); 925 if (sig == null) { 926 sig = modelManager.findPipelinedSignatureAnyByKeys(cs.getFunctionKeys()); 927 } 928 if (sig != null && sig.isResolved() 929 && sig.getStatus() == PipelinedFunctionSignature.ResolutionStatus.OK) { 930 stitchOneCallSite(cs.getTable(), sig); 931 } 932 } 933 934 /** 935 * Finalize hook: process all pending call sites. Called before XML emit. 936 */ 937 public void stitchPendingCallSites() { 938 if (!option.isEnablePipelinedStitching()) return; 939 940 for (PendingPipelinedCallSite cs : modelManager.getPendingPipelinedCallSites()) { 941 stitchOnePending(cs); 942 } 943 } 944 945 private void stitchOneCallSite(TTable table, PipelinedFunctionSignature sig) { 946 Set<String> resolving = modelManager.getResolvingPipelinedFunctions(); 947 if (resolving.contains(sig.getFunctionKey()) 948 || resolving.size() >= option.getMaxPipelinedExpansionDepth()) { 949 return; 950 } 951 952 resolving.add(sig.getFunctionKey()); 953 try { 954 // Get the table model bound to this function call table 955 Object tableModel = modelManager.getModel(table); 956 957 if (tableModel instanceof Table) { 958 Table t = (Table) tableModel; 959 if (t.getColumns() != null && !t.getColumns().isEmpty()) { 960 stitchToTable(t, sig); 961 return; 962 } 963 } 964 965 // For tableExpr tables (TABLE(func())), the table model is created 966 // differently. Try createTableFromCreateDDL which returns existing if bound. 967 Table functionTable = modelFactory.createTableFromCreateDDL(table, true); 968 if (functionTable != null && functionTable.getColumns() != null && !functionTable.getColumns().isEmpty()) { 969 stitchToTable(functionTable, sig); 970 return; 971 } 972 973 // Last resort: create output columns from the signature and stitch 974 if (functionTable != null) { 975 for (String outputCol : sig.getOutputColumns()) { 976 TObjectName colName = new TObjectName(); 977 colName.setString(outputCol); 978 TableColumn column = modelFactory.createTableColumn(functionTable, colName, true); 979 980 List<PipelinedSourceRef> refs = sig.getLineageByOutputColumn().get(outputCol.toLowerCase(Locale.ROOT)); 981 if (refs == null || refs.isEmpty()) continue; 982 983 Set<String> addedSources = new HashSet<>(); 984 int added = 0; 985 for (PipelinedSourceRef ref : refs) { 986 if (added >= option.getMaxStitchedSourcesPerColumn()) break; 987 if (ref.getSourceKind() == PipelinedSourceRef.SourceKind.UNRESOLVED 988 || ref.getSourceKind() == PipelinedSourceRef.SourceKind.CONST) { 989 continue; 990 } 991 String sourceKey = ref.getParentName() + "." + ref.getColumnName(); 992 if (!addedSources.add(sourceKey)) continue; // dedup 993 994 Table sourceTable = modelFactory.createTableByName(ref.getParentName(), false); 995 TObjectName sourceColName = new TObjectName(); 996 sourceColName.setString(ref.getColumnName()); 997 TableColumn sourceCol = modelFactory.createTableColumn(sourceTable, sourceColName, true); 998 999 DataFlowRelationship relation = modelFactory.createDataFlowRelation(); 1000 relation.setEffectType(EffectType.select); 1001 relation.addSource(new TableColumnRelationshipElement(sourceCol)); 1002 relation.setTarget(new TableColumnRelationshipElement(column)); 1003 added++; 1004 } 1005 } 1006 } 1007 } catch (Exception e) { 1008 logger.error("Error stitching pipelined call site for " + sig.getFunctionKey(), e); 1009 } finally { 1010 resolving.remove(sig.getFunctionKey()); 1011 } 1012 } 1013 1014 private void stitchToTable(Table functionTable, PipelinedFunctionSignature sig) { 1015 if (functionTable == null || functionTable.getColumns() == null) return; 1016 1017 for (TableColumn callerCol : functionTable.getColumns()) { 1018 String callerColName = callerCol.getName(); 1019 if (callerColName == null) continue; 1020 1021 String mappedKey = mapByNameThenPosition(callerColName, callerCol, functionTable, sig); 1022 if (mappedKey == null) continue; 1023 1024 List<PipelinedSourceRef> refs = sig.getLineageByOutputColumn().get(mappedKey); 1025 if (refs == null || refs.isEmpty()) continue; 1026 1027 Set<String> addedSources = new HashSet<>(); 1028 int added = 0; 1029 for (PipelinedSourceRef ref : refs) { 1030 if (added >= option.getMaxStitchedSourcesPerColumn()) break; 1031 if (ref.getSourceKind() == PipelinedSourceRef.SourceKind.UNRESOLVED 1032 || ref.getSourceKind() == PipelinedSourceRef.SourceKind.CONST) { 1033 continue; 1034 } 1035 1036 // Dedup: skip if already added 1037 String sourceKey = ref.getParentName() + "." + ref.getColumnName(); 1038 if (!addedSources.add(sourceKey)) continue; 1039 1040 // Create the stitched relationship 1041 Table sourceTable = modelFactory.createTableByName(ref.getParentName(), false); 1042 TObjectName sourceColName = new TObjectName(); 1043 sourceColName.setString(ref.getColumnName()); 1044 TableColumn sourceCol = modelFactory.createTableColumn(sourceTable, sourceColName, true); 1045 1046 DataFlowRelationship relation = modelFactory.createDataFlowRelation(); 1047 relation.setEffectType(EffectType.select); 1048 relation.addSource(new TableColumnRelationshipElement(sourceCol)); 1049 relation.setTarget(new TableColumnRelationshipElement(callerCol)); 1050 1051 added++; 1052 } 1053 } 1054 } 1055 1056 private String mapByNameThenPosition(String callerColName, TableColumn callerCol, 1057 Table functionTable, PipelinedFunctionSignature sig) { 1058 List<String> outputColumns = sig.getOutputColumns(); 1059 if (outputColumns == null || outputColumns.isEmpty()) return null; 1060 1061 // 1. Exact name match 1062 for (String out : outputColumns) { 1063 if (out.equals(callerColName)) return out.toLowerCase(Locale.ROOT); 1064 } 1065 1066 // 2. Normalized name match (case-insensitive, strip quotes) 1067 String normalizedCaller = SQLUtil.trimColumnStringQuote(callerColName.toLowerCase(Locale.ROOT)); 1068 for (String out : outputColumns) { 1069 String normalizedOut = SQLUtil.trimColumnStringQuote(out.toLowerCase(Locale.ROOT)); 1070 if (normalizedOut.equals(normalizedCaller)) return normalizedOut; 1071 } 1072 1073 // 3. Position fallback (strict prerequisites) 1074 if (functionTable.getColumns() != null 1075 && functionTable.getColumns().size() == outputColumns.size()) { 1076 int idx = functionTable.getColumns().indexOf(callerCol); 1077 if (idx >= 0 && idx < outputColumns.size()) { 1078 return outputColumns.get(idx).toLowerCase(Locale.ROOT); 1079 } 1080 } 1081 1082 return null; 1083 } 1084 1085 // ---- Utility methods ---- 1086 1087 /** The names, as written, a call's function may be registered under, most specific first. */ 1088 private List<String> generateFunctionNameCandidates(TFunctionCall funcCall) { 1089 List<String> candidates = new ArrayList<>(); 1090 String funcName = funcCall.getFunctionName().toString().trim(); 1091 1092 // Try various levels of qualification 1093 // Full name as-is 1094 candidates.add(funcName); 1095 1096 // With package prefix from current context 1097 if (ModelBindingManager.getGlobalOraclePackage() != null) { 1098 String pkgName = ModelBindingManager.getGlobalOraclePackage().getName(); 1099 if (pkgName != null && !pkgName.isEmpty()) { 1100 candidates.add(pkgName + "." + funcName); 1101 } 1102 } 1103 1104 // Bare name (strip schema/package prefix) 1105 String bareName = getBareName(funcName); 1106 if (!bareName.equals(funcName)) { 1107 candidates.add(bareName); 1108 } 1109 1110 return candidates; 1111 } 1112 1113 private static List<Object> routineKeys(List<String> names) { 1114 List<Object> keys = new ArrayList<>(names.size()); 1115 for (String name : names) { 1116 keys.add(routineKey(name)); 1117 } 1118 return keys; 1119 } 1120 1121 /** The identity of a type name as written, under the analysis's rules; an Oracle type is named as a table. */ 1122 private static Object typeKey(String written) { 1123 return written == null ? null 1124 : DlineageUtil.lexicalNameKey(ModelBindingManager.getGlobalVendor(), written.trim(), 1125 ESQLDataObjectType.dotTable); 1126 } 1127 1128 /** The identity of a function name as written, package or schema included, under the analysis's rules. */ 1129 private static Object routineKey(String written) { 1130 return written == null ? null 1131 : DlineageUtil.lexicalNameKey(ModelBindingManager.getGlobalVendor(), written.trim(), 1132 ESQLDataObjectType.dotFunction); 1133 } 1134 1135 /** The last segment of a name as written: the name without its schema or package prefix. */ 1136 private static String getBareName(String name) { 1137 if (name == null) return ""; 1138 String written = name.trim(); 1139 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 1140 if (vendor == null) return written; 1141 List<String> segments = QualifiedNameHierarchy.split(vendor, written); 1142 return segments == null || segments.isEmpty() ? written : segments.get(segments.size() - 1); 1143 } 1144}