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