001package gudusoft.gsqlparser.resolver2.expansion;
002
003import gudusoft.gsqlparser.nodes.TExpression;
004import gudusoft.gsqlparser.nodes.TObjectName;
005import gudusoft.gsqlparser.nodes.TResultColumn;
006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
007import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
008import gudusoft.gsqlparser.resolver2.model.ColumnSource;
009import gudusoft.gsqlparser.resolver2.namespace.AbstractNamespace;
010import gudusoft.gsqlparser.resolver2.namespace.INamespace;
011import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
012
013import java.util.ArrayList;
014import java.util.List;
015import java.util.Map;
016
017/**
018 * Expands SELECT * to explicit column lists.
019 *
020 * <p>Star expansion is crucial for:
021 * - Understanding exactly which columns are selected
022 * - Data lineage tracking
023 * - Query refactoring
024 * - Schema evolution analysis
025 *
026 * <p>Supports multiple star expansion scenarios:
027 * 1. Simple star: SELECT * FROM t1
028 * 2. Qualified star: SELECT t1.* FROM t1 JOIN t2
029 * 3. Multiple stars: SELECT t1.*, t2.* FROM t1 JOIN t2
030 * 4. Mixed: SELECT t1.*, t2.id FROM t1 JOIN t2
031 * 5. Nested: SELECT * FROM (SELECT * FROM t1) sub
032 *
033 * <p>Example:
034 * <pre>
035 * -- Input:
036 * SELECT * FROM employees
037 *
038 * -- Expanded (if metadata available):
039 * SELECT employees.id, employees.name, employees.salary
040 * FROM employees
041 * </pre>
042 */
043public class StarColumnExpander {
044
045    /** Whether to include table qualifiers in expanded columns */
046    private final boolean includeTableQualifier;
047
048    /** Whether to expand stars in subqueries recursively */
049    private final boolean expandSubqueries;
050
051    public StarColumnExpander() {
052        this(true, true);
053    }
054
055    public StarColumnExpander(boolean includeTableQualifier, boolean expandSubqueries) {
056        this.includeTableQualifier = includeTableQualifier;
057        this.expandSubqueries = expandSubqueries;
058    }
059
060    /**
061     * Expand a star column to explicit column list.
062     *
063     * @param starColumn the star column (SELECT *)
064     * @param namespace the namespace to expand from
065     * @return list of expanded columns, or empty if expansion not possible
066     */
067    public List<ExpandedColumn> expandStar(TResultColumn starColumn, INamespace namespace) {
068        List<ExpandedColumn> expandedColumns = new ArrayList<>();
069
070        if (starColumn == null || namespace == null) {
071            return expandedColumns;
072        }
073
074        // Check if this is actually a star column
075        if (!isStarColumn(starColumn)) {
076            return expandedColumns;
077        }
078
079        // Get all columns from the namespace
080        Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
081
082        if (columnSources == null || columnSources.isEmpty()) {
083            return expandedColumns;
084        }
085
086        // Create expanded column for each source column
087        for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
088            String columnName = entry.getKey();
089            ColumnSource source = entry.getValue();
090
091            ExpandedColumn expanded = new ExpandedColumn(
092                columnName,
093                source,
094                starColumn,
095                includeTableQualifier
096            );
097
098            expandedColumns.add(expanded);
099        }
100
101        return expandedColumns;
102    }
103
104    /**
105     * Expand qualified star (e.g., t1.*) to explicit column list.
106     *
107     * @param starColumn the star column (SELECT t1.*)
108     * @param namespace the namespace to expand from
109     * @param tableAlias the table qualifier (e.g., "t1")
110     * @return list of expanded columns, or empty if expansion not possible
111     */
112    public List<ExpandedColumn> expandQualifiedStar(
113            TResultColumn starColumn,
114            INamespace namespace,
115            String tableAlias) {
116
117        List<ExpandedColumn> expandedColumns = new ArrayList<>();
118
119        if (starColumn == null || namespace == null || tableAlias == null) {
120            return expandedColumns;
121        }
122
123        // Get all columns from the namespace
124        Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
125
126        if (columnSources == null || columnSources.isEmpty()) {
127            return expandedColumns;
128        }
129
130        // Create expanded column for each source column
131        for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
132            String columnName = entry.getKey();
133            ColumnSource source = entry.getValue();
134
135            ExpandedColumn expanded = new ExpandedColumn(
136                columnName,
137                source,
138                starColumn,
139                tableAlias,
140                includeTableQualifier
141            );
142
143            expandedColumns.add(expanded);
144        }
145
146        return expandedColumns;
147    }
148
149    /**
150     * Expand all stars in a result column list.
151     *
152     * @param resultColumns the result columns
153     * @param availableNamespaces list of available namespaces to expand from
154     * @return expansion result with all expanded columns
155     */
156    public ExpansionResult expandAllStars(
157            List<TResultColumn> resultColumns,
158            List<INamespace> availableNamespaces) {
159
160        ExpansionResult result = new ExpansionResult();
161
162        if (resultColumns == null || availableNamespaces == null) {
163            return result;
164        }
165
166        for (TResultColumn rc : resultColumns) {
167            if (isStarColumn(rc)) {
168                // This is a star column - expand it
169                if (isQualifiedStar(rc)) {
170                    // Qualified star: t1.*
171                    String tableAlias = getStarQualifier(rc);
172                    INamespace targetNamespace = findNamespaceByAlias(
173                        availableNamespaces,
174                        tableAlias
175                    );
176
177                    if (targetNamespace != null) {
178                        List<ExpandedColumn> expanded = expandQualifiedStar(
179                            rc,
180                            targetNamespace,
181                            tableAlias
182                        );
183                        result.addExpandedStar(rc, expanded);
184                    } else {
185                        result.addUnexpandableStar(rc, "Namespace not found: " + tableAlias);
186                    }
187                } else {
188                    // Unqualified star: *
189                    // Expand from all available namespaces
190                    List<ExpandedColumn> allExpanded = new ArrayList<>();
191                    for (INamespace ns : availableNamespaces) {
192                        List<ExpandedColumn> expanded = expandStar(rc, ns);
193                        allExpanded.addAll(expanded);
194                    }
195
196                    if (!allExpanded.isEmpty()) {
197                        result.addExpandedStar(rc, allExpanded);
198                    } else {
199                        result.addUnexpandableStar(rc, "No columns available for expansion");
200                    }
201                }
202            } else {
203                // Not a star column - keep as is
204                result.addNonStarColumn(rc);
205            }
206        }
207
208        return result;
209    }
210
211    /**
212     * Check if a result column is a star column (* or t.*)
213     */
214    private boolean isStarColumn(TResultColumn rc) {
215        if (rc == null || rc.getExpr() == null) {
216            return false;
217        }
218
219        TExpression expr = rc.getExpr();
220        if (expr.getExpressionType() != gudusoft.gsqlparser.EExpressionType.simple_object_name_t) {
221            return false;
222        }
223
224        TObjectName objName = expr.getObjectOperand();
225        if (objName == null) {
226            return false;
227        }
228
229        String str = objName.toString();
230        return str != null && (str.equals("*") || str.endsWith(".*"));
231    }
232
233    /**
234     * Check if a star column is qualified (t.*)
235     */
236    private boolean isQualifiedStar(TResultColumn rc) {
237        if (!isStarColumn(rc)) {
238            return false;
239        }
240
241        TObjectName objName = rc.getExpr().getObjectOperand();
242        String str = objName.toString();
243        return str != null && str.endsWith(".*") && str.length() > 2;
244    }
245
246    /**
247     * Get the qualifier from a qualified star (t.* returns "t")
248     */
249    private String getStarQualifier(TResultColumn rc) {
250        if (!isQualifiedStar(rc)) {
251            return null;
252        }
253
254        TObjectName objName = rc.getExpr().getObjectOperand();
255        String str = objName.toString();
256        if (str != null && str.endsWith(".*")) {
257            return str.substring(0, str.length() - 2);
258        }
259
260        return null;
261    }
262
263    /**
264     * Find a namespace by alias/name.
265     *
266     * <p>Slice S2: route compares through the namespace's {@link INameMatcher}
267     * with {@link ESQLDataObjectType#dotTable} so BigQuery's table-sensitive
268     * rule and Oracle / Postgres quoted-sensitive rules are honored. Falls
269     * back to {@link String#equalsIgnoreCase} for synthetic test scopes
270     * whose namespaces are not {@link AbstractNamespace}.</p>
271     */
272    private INamespace findNamespaceByAlias(List<INamespace> namespaces, String alias) {
273        if (namespaces == null || alias == null) {
274            return null;
275        }
276
277        for (INamespace ns : namespaces) {
278            INameMatcher matcher = ns instanceof AbstractNamespace
279                    ? ((AbstractNamespace) ns).getNameMatcher()
280                    : null;
281
282            // Check if namespace display name matches
283            if (ns.getDisplayName() != null &&
284                tableMatches(matcher, ns.getDisplayName(), alias)) {
285                return ns;
286            }
287
288            // For table namespaces, check table name/alias
289            if (ns.getFinalTable() != null) {
290                String tableName = ns.getFinalTable().getName();
291                String tableAlias = ns.getFinalTable().getAliasName();
292
293                if ((tableName != null && tableMatches(matcher, tableName, alias)) ||
294                    (tableAlias != null && tableMatches(matcher, tableAlias, alias))) {
295                    return ns;
296                }
297            }
298        }
299
300        return null;
301    }
302
303    private static boolean tableMatches(INameMatcher matcher, String stored, String input) {
304        if (stored == null || input == null) {
305            return stored == input;
306        }
307        if (matcher instanceof VendorNameMatcher) {
308            return ((VendorNameMatcher) matcher).matches(stored, input, ESQLDataObjectType.dotTable);
309        }
310        if (matcher != null) {
311            return matcher.matches(stored, input);
312        }
313        return stored.equalsIgnoreCase(input);
314    }
315}