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}