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}