001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.nodes.TObjectName; 004import gudusoft.gsqlparser.nodes.TObjectNameList; 005import gudusoft.gsqlparser.nodes.TResultColumn; 006import gudusoft.gsqlparser.nodes.TResultColumnList; 007import gudusoft.gsqlparser.nodes.TTable; 008import gudusoft.gsqlparser.nodes.TPivotClause; 009import gudusoft.gsqlparser.nodes.TPivotInClause; 010import gudusoft.gsqlparser.nodes.TUnpivotInClause; 011import gudusoft.gsqlparser.nodes.TUnpivotInClauseItem; 012import gudusoft.gsqlparser.nodes.TExpression; 013import gudusoft.gsqlparser.nodes.TExpressionList; 014import gudusoft.gsqlparser.nodes.TFunctionCall; 015import gudusoft.gsqlparser.nodes.TConstant; 016import gudusoft.gsqlparser.EDbVendor; 017import gudusoft.gsqlparser.EExpressionType; 018import gudusoft.gsqlparser.resolver2.ColumnLevel; 019import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 020import gudusoft.gsqlparser.resolver2.model.ColumnSource; 021 022import java.util.*; 023 024/** 025 * Namespace representing a PIVOT table. 026 * 027 * <p>A PIVOT transforms rows into columns using an aggregate function: 028 * <pre> 029 * SELECT ... 030 * FROM source_table 031 * PIVOT (aggregate_function(value_column) FOR pivot_column IN (val1, val2, ...)) AS alias 032 * </pre> 033 * 034 * <p>The PIVOT produces: 035 * <ul> 036 * <li>Columns from the IN clause (val1, val2, ...) - these are the pivoted columns</li> 037 * <li>Pass-through columns from the source table (all columns NOT used in aggregate or FOR clause)</li> 038 * </ul> 039 * 040 * <p>For column resolution purposes: 041 * <ul> 042 * <li>Pivot columns (from IN clause) resolve to the PivotNamespace</li> 043 * <li>Other columns may come from the source table (pass-through columns)</li> 044 * </ul> 045 */ 046public class PivotNamespace extends AbstractNamespace { 047 048 /** The pivot table this namespace represents */ 049 private final TTable pivotTable; 050 051 /** The source table that the PIVOT operates on */ 052 private final TTable sourceTable; 053 054 /** The PIVOT clause */ 055 private final TPivotClause pivotClause; 056 057 /** Alias for this pivot table (from AS clause) */ 058 private final String alias; 059 060 /** Namespace for the source table (for pass-through column resolution) */ 061 private INamespace sourceNamespace; 062 063 /** Columns defined by the PIVOT IN clause */ 064 private final Set<String> pivotColumns = new LinkedHashSet<>(); 065 066 /** Columns consumed by UNPIVOT IN (...) list (these do NOT exist as output columns) */ 067 private final Set<String> unpivotInColumns = new LinkedHashSet<>(); 068 069 /** Display name format for PIVOT table */ 070 public static final String PIVOT_TABLE_SUFFIX = "(piviot_table)"; 071 072 /** Display name format for UNPIVOT table */ 073 public static final String UNPIVOT_TABLE_SUFFIX = "(unpivot_table)"; 074 075 /** Whether this is an UNPIVOT clause */ 076 private boolean isUnpivot = false; 077 078 public PivotNamespace(TTable pivotTable, TPivotClause pivotClause, 079 TTable sourceTable, String alias, INameMatcher nameMatcher) { 080 super(pivotTable, nameMatcher); 081 this.pivotTable = pivotTable; 082 this.pivotClause = pivotClause; 083 this.sourceTable = sourceTable; 084 this.alias = alias; 085 // Check if this is an UNPIVOT clause 086 if (pivotClause != null) { 087 this.isUnpivot = (pivotClause.getType() == TPivotClause.unpivot); 088 } 089 } 090 091 /** 092 * Check if this is an UNPIVOT namespace. 093 */ 094 public boolean isUnpivot() { 095 return isUnpivot; 096 } 097 098 /** 099 * Set the namespace representing the source table. 100 * This is used for resolving pass-through columns. 101 */ 102 public void setSourceNamespace(INamespace sourceNamespace) { 103 this.sourceNamespace = sourceNamespace; 104 } 105 106 /** 107 * Get the namespace representing the source table. 108 */ 109 public INamespace getSourceNamespace() { 110 return sourceNamespace; 111 } 112 113 /** 114 * Get the pivot table. 115 */ 116 public TTable getPivotTable() { 117 return pivotTable; 118 } 119 120 /** 121 * Get the underlying table that PIVOT operates on. 122 * This is the table whose columns are being pivoted. 123 */ 124 public TTable getUnderlyingSourceTable() { 125 return sourceTable; 126 } 127 128 /** 129 * {@inheritDoc} 130 * For PivotNamespace, returns the PIVOT table (the result of the PIVOT operation). 131 * This is the immediate source table for columns resolved through this PIVOT. 132 */ 133 @Override 134 public TTable getSourceTable() { 135 return pivotTable; 136 } 137 138 /** 139 * Get the PIVOT clause. 140 */ 141 public TPivotClause getPivotClause() { 142 return pivotClause; 143 } 144 145 /** 146 * Get the set of pivot column names (from IN clause). 147 */ 148 public Set<String> getPivotColumns() { 149 return Collections.unmodifiableSet(pivotColumns); 150 } 151 152 /** 153 * Check if a column is a pivot column (from IN clause). 154 */ 155 public boolean isPivotColumn(String columnName) { 156 if (columnName == null) return false; 157 for (String pivotCol : pivotColumns) { 158 if (nameMatcher.matches(pivotCol, columnName)) { 159 return true; 160 } 161 } 162 return false; 163 } 164 165 @Override 166 public String getDisplayName() { 167 String suffix = isUnpivot ? UNPIVOT_TABLE_SUFFIX : PIVOT_TABLE_SUFFIX; 168 if (alias != null && !alias.isEmpty()) { 169 return alias + suffix; 170 } 171 return (isUnpivot ? "unpivot_alias" : "pivot_alias") + suffix; 172 } 173 174 @Override 175 public TTable getFinalTable() { 176 // Return the pivot table itself so that pivot columns are properly attributed 177 // to the pivot table (e.g., "(pivot-table:p(piviot_table)).[1]") 178 // For lineage tracing to source columns, use getAllFinalTables() 179 return pivotTable; 180 } 181 182 @Override 183 public List<TTable> getAllFinalTables() { 184 // Return source table for lineage tracing 185 if (sourceTable != null) { 186 List<TTable> tables = new ArrayList<>(); 187 tables.add(sourceTable); 188 return tables; 189 } 190 return Collections.emptyList(); 191 } 192 193 @Override 194 protected void doValidate() { 195 columnSources = new LinkedHashMap<>(); 196 pivotColumns.clear(); 197 unpivotInColumns.clear(); 198 199 if (pivotClause == null) { 200 return; 201 } 202 203 if (isUnpivot) { 204 // UNPIVOT case: Add generated columns (value column and FOR column) 205 // UNPIVOT (yearly_total FOR order_mode IN (store AS 'direct', internet AS 'online')) 206 // - yearly_total is the value column (new column containing values) 207 // - order_mode is the FOR column (new column containing labels) 208 209 // Add value columns (e.g., yearly_total) 210 // Note: For single value column UNPIVOT (like Oracle), use getValueColumn() (deprecated singular) 211 // For multi-value column UNPIVOT (like SQL Server), use getValueColumnList() 212 TObjectNameList valueColumns = pivotClause.getValueColumnList(); 213 if (valueColumns != null && valueColumns.size() > 0) { 214 for (int i = 0; i < valueColumns.size(); i++) { 215 TObjectName valueCol = valueColumns.getObjectName(i); 216 if (valueCol != null) { 217 String columnName = valueCol.getColumnNameOnly(); 218 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 219 addPivotColumn(columnName, "unpivot_value_column"); 220 } 221 } 222 } 223 } else { 224 // Fallback to deprecated singular method for Oracle compatibility 225 @SuppressWarnings("deprecation") 226 TObjectName valueCol = pivotClause.getValueColumn(); 227 if (valueCol != null) { 228 String columnName = valueCol.getColumnNameOnly(); 229 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 230 addPivotColumn(columnName, "unpivot_value_column"); 231 } 232 } 233 } 234 235 // Add FOR columns (e.g., order_mode) 236 // Note: For single FOR column UNPIVOT (like Oracle), use getPivotColumn() (deprecated singular) 237 // For multi-FOR column UNPIVOT, use getPivotColumnList() 238 TObjectNameList pivotColumnList = pivotClause.getPivotColumnList(); 239 if (pivotColumnList != null && pivotColumnList.size() > 0) { 240 for (int i = 0; i < pivotColumnList.size(); i++) { 241 TObjectName forCol = pivotColumnList.getObjectName(i); 242 if (forCol != null) { 243 String columnName = forCol.getColumnNameOnly(); 244 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 245 addPivotColumn(columnName, "unpivot_for_column"); 246 } 247 } 248 } 249 } else { 250 // Fallback to deprecated singular method for Oracle compatibility 251 @SuppressWarnings("deprecation") 252 TObjectName forCol = pivotClause.getPivotColumn(); 253 if (forCol != null) { 254 String columnName = forCol.getColumnNameOnly(); 255 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 256 addPivotColumn(columnName, "unpivot_for_column"); 257 } 258 } 259 } 260 261 // UNPIVOT case: Collect consumed IN(...) source columns so they are NOT treated as pass-through 262 // Example: UNPIVOT (col3 FOR col4 IN (p.col2, p.col3)) 263 // - p.col2, p.col3 are consumed source columns and should not be visible via unpivot alias 264 TUnpivotInClause unpivotInClause = pivotClause.getUnpivotInClause(); 265 if (unpivotInClause != null && unpivotInClause.getItems() != null) { 266 for (int i = 0; i < unpivotInClause.getItems().size(); i++) { 267 TUnpivotInClauseItem item = unpivotInClause.getItems().getElement(i); 268 if (item == null) continue; 269 270 if (item.getColumn() != null) { 271 String name = item.getColumn().getColumnNameOnly(); 272 if (name != null && !name.isEmpty()) { 273 unpivotInColumns.add(name); 274 } 275 } 276 if (item.getColumnList() != null) { 277 TObjectNameList cols = item.getColumnList(); 278 for (int j = 0; j < cols.size(); j++) { 279 TObjectName col = cols.getObjectName(j); 280 if (col != null) { 281 String name = col.getColumnNameOnly(); 282 if (name != null && !name.isEmpty()) { 283 unpivotInColumns.add(name); 284 } 285 } 286 } 287 } 288 } 289 } 290 } else { 291 // PIVOT case: Get output columns 292 // If alias clause has a column list (e.g., AS p (col1, col2, col3)), use those 293 // as they REPLACE the original pivot column names. Otherwise use IN clause columns. 294 boolean hasAliasColumnList = pivotClause.getAliasClause() != null && 295 pivotClause.getAliasClause().getColumns() != null && 296 pivotClause.getAliasClause().getColumns().size() > 0; 297 298 if (hasAliasColumnList) { 299 // Alias column list replaces the default pivot column names 300 // e.g., PIVOT(...) AS p (empid_renamed, Q1, Q2, Q3, Q4) 301 // The alias columns provide the output column names 302 for (TObjectName column : pivotClause.getAliasClause().getColumns()) { 303 String columnName = stripDelimiters(column.toString()); 304 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 305 addPivotColumn(columnName, "pivot_alias_clause"); 306 } 307 } 308 } else { 309 // No alias column list - use IN clause columns as pivot column names 310 TPivotInClause inClause = pivotClause.getPivotInClause(); 311 if (inClause != null) { 312 // Case 1: IN clause has items (e.g., IN ([1], [2], [3]) or IN ([Sammich], [Pickle])) 313 if (inClause.getItems() != null) { 314 TResultColumnList items = inClause.getItems(); 315 for (int i = 0; i < items.size(); i++) { 316 TResultColumn resultColumn = items.getResultColumn(i); 317 String columnName = extractPivotColumnName(resultColumn); 318 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 319 addPivotColumn(columnName, "pivot_in_clause"); 320 } 321 } 322 } 323 324 // Case 2: IN clause has a subquery (e.g., IN (SELECT DISTINCT col FROM table)) 325 if (inClause.getSubQuery() != null) { 326 TResultColumnList subqueryColumns = inClause.getSubQuery().getResultColumnList(); 327 if (subqueryColumns != null) { 328 for (int i = 0; i < subqueryColumns.size(); i++) { 329 TResultColumn resultColumn = subqueryColumns.getResultColumn(i); 330 String columnName = stripDelimiters(resultColumn.getDisplayName()); 331 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 332 addPivotColumn(columnName, "pivot_in_subquery"); 333 } 334 } 335 } 336 } 337 338 // Case 3: IN clause has value list (e.g., IN ((val1, val2), (val3, val4))) 339 if (inClause.getValueList() != null) { 340 for (TExpressionList exprList : inClause.getValueList()) { 341 if (exprList != null) { 342 for (int i = 0; i < exprList.size(); i++) { 343 TExpression expr = exprList.getExpression(i); 344 String columnName = extractColumnNameFromExpression(expr); 345 if (columnName != null && !columnName.isEmpty() && !containsColumnByMatcher(columnSources, columnName)) { 346 addPivotColumn(columnName, "pivot_in_valuelist"); 347 } 348 } 349 } 350 } 351 } 352 } 353 } 354 } 355 } 356 357 /** 358 * Extract the pivot column name from a result column in the IN clause. 359 * Handles both column references and constant values. 360 */ 361 private String extractPivotColumnName(TResultColumn resultColumn) { 362 if (resultColumn == null) return null; 363 364 // An explicit alias names the generated column outright. 365 String alias = inListItemAlias(resultColumn); 366 if (alias != null) { 367 return alias; 368 } 369 370 TExpression expr = resultColumn.getExpr(); 371 if (expr != null) { 372 return extractColumnNameFromExpression(expr); 373 } 374 375 // Fallback to display name 376 return stripDelimiters(resultColumn.getDisplayName()); 377 } 378 379 /** 380 * The explicit alias of one PIVOT IN-list item, or null when it has none. 381 * 382 * <p>{@code IN ('direct' AS Store)} names the generated column {@code Store}. 383 * Oracle, Snowflake, BigQuery and Teradata all accept this form and in every one 384 * of them the alias — not the value — is the output column name, so it takes 385 * precedence over any value-derived naming.</p> 386 * 387 * <p>Shared with {@code ScopeBuilder.addPivotInClauseColumns} and matched by 388 * {@code TPivotInClause.linkColumnToTable} on purpose. The name a pivot column 389 * is <em>published</em> under and the name it can be <em>resolved</em> by have 390 * to be produced by the same rule, or a reference to the real column reports 391 * NOT_FOUND while a stale name still matches.</p> 392 * 393 * <p>The alias is returned <em>verbatim</em>, quote state included. Stripping it 394 * would be wrong in both directions for {@code IN ('x' AS "Store")}: bare 395 * {@code SELECT Store} would become a false exact match, and the only valid 396 * reference, {@code SELECT "Store"}, would report NOT_FOUND. Retaining quote 397 * state is what the identifier normalization guide requires; comparison is the 398 * matcher's job, not this method's.</p> 399 */ 400 public static String inListItemAlias(TResultColumn resultColumn) { 401 if (resultColumn == null 402 || resultColumn.getAliasClause() == null 403 || resultColumn.getAliasClause().getAliasName() == null) { 404 return null; 405 } 406 String alias = resultColumn.getAliasClause().getAliasName().toString(); 407 return (alias == null || alias.isEmpty()) ? null : alias; 408 } 409 410 /** 411 * The name a PIVOT gives to the column generated by one unaliased IN-list value. 412 * 413 * <p>Vendor-specific, and the difference is not cosmetic. Oracle and Snowflake 414 * name the column after the literal <em>as written</em>, quotes included, so 415 * {@code IN ('direct')} yields a column really named {@code 'direct'} that must 416 * be referenced as {@code "'direct'"} (Mantis 4662). BigQuery requires its 417 * IN-list values to form valid identifiers and names the column {@code direct}, 418 * so the quotes are still stripped there and for every other vendor.</p> 419 * 420 * <p>Shared with {@code ScopeBuilder} for the same reason as 421 * {@link #inListItemAlias}.</p> 422 */ 423 public static String inListValueColumnName(EDbVendor vendor, String rawValue) { 424 if (vendor == EDbVendor.dbvsnowflake || vendor == EDbVendor.dbvoracle) { 425 return rawValue; 426 } 427 return stripDelimitersFrom(rawValue); 428 } 429 430 /** Strip a leading+trailing quote pair. SQL Server brackets are part of the identity. */ 431 public static String stripDelimitersFrom(String name) { 432 if (name == null || name.isEmpty()) { 433 return name; 434 } 435 // Strip double quotes (BigQuery style identifier) 436 if (name.startsWith("\"") && name.endsWith("\"") && name.length() > 2) { 437 return name.substring(1, name.length() - 1); 438 } 439 // Strip single quotes (string literal) 440 if (name.startsWith("'") && name.endsWith("'") && name.length() > 2) { 441 return name.substring(1, name.length() - 1); 442 } 443 // SQL Server brackets [] are preserved as they're part of the column identity 444 return name; 445 } 446 447 /** 448 * Extract column name from an expression (handles constants and column references). 449 */ 450 private String extractColumnNameFromExpression(TExpression expr) { 451 if (expr == null) return null; 452 453 EExpressionType exprType = expr.getExpressionType(); 454 455 // Column reference (e.g., [Sammich], [Apple], "HOUSTON" in BigQuery) 456 // For BigQuery, double-quoted identifiers like "HOUSTON" are parsed as simple_object_name_t 457 // and we need to strip the quotes for display purposes. 458 if (exprType == EExpressionType.simple_object_name_t) { 459 TObjectName objName = expr.getObjectOperand(); 460 if (objName != null) { 461 // Strip delimiters (quotes) for display - per identifier_normalization.md 462 return stripDelimiters(objName.toString()); 463 } 464 } 465 466 // Constant value (e.g., [1], [2], 'value', "HOUSTON") 467 if (exprType == EExpressionType.simple_constant_t) { 468 TConstant constant = expr.getConstantOperand(); 469 if (constant != null && constant.getValueToken() != null) { 470 // Vendor-specific: Oracle/Snowflake keep the literal as written. 471 return inListValueColumnName(expr.dbvendor, constant.getValueToken().toString()); 472 } 473 } 474 475 // Fallback: use expression string with delimiters stripped 476 return stripDelimiters(expr.toString()); 477 } 478 479 /** 480 * Add a pivot column to the namespace. 481 */ 482 private void addPivotColumn(String columnName, String evidence) { 483 pivotColumns.add(columnName); 484 ColumnSource source = new ColumnSource( 485 this, 486 columnName, 487 null, 488 1.0, // Definite - from PIVOT IN clause 489 evidence 490 ); 491 columnSources.put(columnName, source); 492 } 493 494 /** 495 * Strip SQL string delimiters (double quotes and single quotes) from a name. 496 * Note: SQL Server brackets and backticks are preserved as they're often 497 * needed for special names like [1], [Date], etc. 498 */ 499 private String stripDelimiters(String name) { 500 if (name == null || name.isEmpty()) { 501 return name; 502 } 503 // Strip double quotes (BigQuery style identifier) 504 if (name.startsWith("\"") && name.endsWith("\"") && name.length() > 2) { 505 return name.substring(1, name.length() - 1); 506 } 507 // Strip single quotes (string literal) 508 if (name.startsWith("'") && name.endsWith("'") && name.length() > 2) { 509 return name.substring(1, name.length() - 1); 510 } 511 // SQL Server brackets [] are preserved as they're part of the column identity 512 return name; 513 } 514 515 @Override 516 public ColumnLevel hasColumn(String columnName) { 517 ensureValidated(); 518 519 // First check if it's a pivot column 520 if (isPivotColumn(columnName)) { 521 return ColumnLevel.EXISTS; 522 } 523 524 // UNPIVOT: IN(...) columns are consumed and do NOT exist in the virtual table output 525 if (isUnpivot && isUnpivotInColumn(columnName)) { 526 return ColumnLevel.NOT_EXISTS; 527 } 528 529 // Check in columnSources 530 for (String existingCol : columnSources.keySet()) { 531 if (nameMatcher.matches(existingCol, columnName)) { 532 return ColumnLevel.EXISTS; 533 } 534 } 535 536 // For PIVOT tables without explicit column list, any column MAYBE exists 537 // (pass-through columns from source table) 538 return ColumnLevel.MAYBE; 539 } 540 541 @Override 542 public ColumnSource resolveColumn(String columnName) { 543 ensureValidated(); 544 545 // First try to find in pivot columns (from IN clause) 546 for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) { 547 if (nameMatcher.matches(entry.getKey(), columnName)) { 548 return entry.getValue(); 549 } 550 } 551 552 // UNPIVOT: IN(...) columns are consumed and must NOT resolve as pass-through columns 553 if (isUnpivot && isUnpivotInColumn(columnName)) { 554 return null; 555 } 556 557 // Delta 2: Delegate to source namespace for pass-through columns 558 // Pass-through columns are all source columns EXCEPT: 559 // - The FOR column (pivot_column) 560 // - The aggregate input column (value_column) 561 if (sourceNamespace != null) { 562 // Check if this is a pass-through column (not FOR or aggregate column) 563 if (!isForColumn(columnName) && !isAggregateInputColumn(columnName)) { 564 ColumnSource sourceColumn = sourceNamespace.resolveColumn(columnName); 565 if (sourceColumn != null) { 566 // Both PIVOT and UNPIVOT pass-through columns keep their source table attribution 567 // (e.g., SELECT year FROM ... UNPIVOT ... -> year attributed to source table) 568 // This matches the expected behavior where passthrough columns should be 569 // attributed to their original source table, not the PIVOT/UNPIVOT virtual table. 570 return sourceColumn; 571 } 572 } 573 } 574 575 return null; 576 } 577 578 private boolean isUnpivotInColumn(String columnName) { 579 if (columnName == null || columnName.isEmpty()) return false; 580 for (String consumed : unpivotInColumns) { 581 if (nameMatcher.matches(consumed, columnName)) { 582 return true; 583 } 584 } 585 return false; 586 } 587 588 /** 589 * Check if the column is the FOR column (pivot_column) in the PIVOT clause. 590 * The FOR column is consumed by PIVOT and should not be passed through. 591 */ 592 private boolean isForColumn(String columnName) { 593 if (pivotClause == null || columnName == null) { 594 return false; 595 } 596 597 // Try pivotColumnList first (modern API, used by BigQuery and others) 598 TObjectNameList pivotColumns = pivotClause.getPivotColumnList(); 599 if (pivotColumns != null && pivotColumns.size() > 0) { 600 for (int i = 0; i < pivotColumns.size(); i++) { 601 TObjectName forColumn = pivotColumns.getObjectName(i); 602 if (forColumn != null) { 603 String forColName = forColumn.getColumnNameOnly(); 604 if (forColName != null && nameMatcher.matches(forColName, columnName)) { 605 return true; 606 } 607 } 608 } 609 } 610 611 // Fallback to deprecated singular method 612 @SuppressWarnings("deprecation") 613 TObjectName forColumn = pivotClause.getPivotColumn(); 614 if (forColumn != null) { 615 String forColName = forColumn.getColumnNameOnly(); 616 if (forColName != null && nameMatcher.matches(forColName, columnName)) { 617 return true; 618 } 619 } 620 return false; 621 } 622 623 /** 624 * Check if the column is the aggregate input column (value_column) in the PIVOT clause. 625 * The aggregate input column is consumed by PIVOT and should not be passed through. 626 */ 627 private boolean isAggregateInputColumn(String columnName) { 628 if (pivotClause == null || columnName == null) { 629 return false; 630 } 631 632 // Try to extract columns from aggregate function (SQL Server / BigQuery style) 633 // e.g., PIVOT(AVG(departure_delay) FOR airline IN (...)) 634 TFunctionCall aggFunc = pivotClause.getAggregation_function(); 635 if (aggFunc != null && aggFunc.getArgs() != null) { 636 for (int i = 0; i < aggFunc.getArgs().size(); i++) { 637 TExpression argExpr = aggFunc.getArgs().getExpression(i); 638 if (argExpr != null && argExpr.getObjectOperand() != null) { 639 TObjectName argCol = argExpr.getObjectOperand(); 640 String argColName = argCol.getColumnNameOnly(); 641 if (argColName != null && nameMatcher.matches(argColName, columnName)) { 642 return true; 643 } 644 } 645 } 646 } 647 648 // Try to extract columns from aggregate function list (Oracle style) 649 TResultColumnList aggFuncList = pivotClause.getAggregation_function_list(); 650 if (aggFuncList != null) { 651 for (int i = 0; i < aggFuncList.size(); i++) { 652 TResultColumn rc = aggFuncList.getResultColumn(i); 653 if (rc != null && rc.getExpr() != null) { 654 TExpression expr = rc.getExpr(); 655 if (expr.getFunctionCall() != null && expr.getFunctionCall().getArgs() != null) { 656 for (int j = 0; j < expr.getFunctionCall().getArgs().size(); j++) { 657 TExpression argExpr = expr.getFunctionCall().getArgs().getExpression(j); 658 if (argExpr != null && argExpr.getObjectOperand() != null) { 659 TObjectName argCol = argExpr.getObjectOperand(); 660 String argColName = argCol.getColumnNameOnly(); 661 if (argColName != null && nameMatcher.matches(argColName, columnName)) { 662 return true; 663 } 664 } 665 } 666 } 667 } 668 } 669 } 670 671 // Fallback: use valueColumnList (for UNPIVOT or direct value column specification) 672 TObjectNameList valueColumns = pivotClause.getValueColumnList(); 673 if (valueColumns != null && valueColumns.size() > 0) { 674 for (int i = 0; i < valueColumns.size(); i++) { 675 TObjectName valueCol = valueColumns.getObjectName(i); 676 if (valueCol != null) { 677 String valueColName = valueCol.getColumnNameOnly(); 678 if (valueColName != null && nameMatcher.matches(valueColName, columnName)) { 679 return true; 680 } 681 } 682 } 683 } 684 685 // Fallback to deprecated singular method 686 @SuppressWarnings("deprecation") 687 TObjectName valueCol = pivotClause.getValueColumn(); 688 if (valueCol != null) { 689 String valueColName = valueCol.getColumnNameOnly(); 690 if (valueColName != null && nameMatcher.matches(valueColName, columnName)) { 691 return true; 692 } 693 } 694 return false; 695 } 696 697 /** 698 * Get all column sources for this PIVOT namespace. 699 * This includes both pivot columns (from IN clause) and pass-through columns from the source. 700 * 701 * <p>For PIVOT, the output columns are: 702 * <ul> 703 * <li>Pivot columns: generated from IN clause values (e.g., 'AA', 'KH', 'DL', '9E')</li> 704 * <li>Pass-through columns: source columns NOT consumed by FOR or aggregate</li> 705 * </ul> 706 * 707 * <p>This method is critical for star column expansion: when outer query uses SELECT * 708 * from a PIVOT table, we need to return ALL columns the pivot table exposes. 709 */ 710 @Override 711 public Map<String, ColumnSource> getAllColumnSources() { 712 ensureValidated(); 713 714 if (columnSources == null) { 715 return Collections.emptyMap(); 716 } 717 718 // Start with pivot columns (from IN clause) 719 Map<String, ColumnSource> allColumns = new LinkedHashMap<>(columnSources); 720 721 // Add pass-through columns from source namespace 722 // Pass-through = all source columns EXCEPT FOR column and aggregate input column 723 if (sourceNamespace != null) { 724 Map<String, ColumnSource> sourceColumns = sourceNamespace.getAllColumnSources(); 725 for (Map.Entry<String, ColumnSource> entry : sourceColumns.entrySet()) { 726 String colName = entry.getKey(); 727 728 // Skip if already a pivot column (from IN clause). Slice S1: 729 // matcher-aware containment so case-only-different source 730 // column names don't slip past on case-insensitive vendors. 731 if (containsColumnByMatcher(allColumns, colName)) { 732 continue; 733 } 734 735 // Skip FOR column (consumed by PIVOT) 736 if (isForColumn(colName)) { 737 continue; 738 } 739 740 // Skip aggregate input column (consumed by PIVOT) 741 if (isAggregateInputColumn(colName)) { 742 continue; 743 } 744 745 // This is a pass-through column - add it with pivot table attribution 746 ColumnSource sourceCol = entry.getValue(); 747 ColumnSource passthroughCol = new ColumnSource( 748 this, // PivotNamespace 749 colName, 750 sourceCol.getDefinitionNode(), 751 sourceCol.getConfidence(), 752 "pivot_passthrough", 753 pivotTable, // attribute to pivot table 754 null 755 ); 756 allColumns.put(colName, passthroughCol); 757 } 758 } 759 return Collections.unmodifiableMap(allColumns); 760 } 761 762 @Override 763 public String toString() { 764 return "PivotNamespace(" + getDisplayName() + ", pivotCols=" + pivotColumns.size() + ")"; 765 } 766}