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