001package gudusoft.gsqlparser.resolver2.expansion;
002
003import gudusoft.gsqlparser.nodes.TObjectName;
004import gudusoft.gsqlparser.nodes.TResultColumn;
005import gudusoft.gsqlparser.nodes.TResultColumnList;
006import gudusoft.gsqlparser.resolver2.ScopeBuildResult;
007import gudusoft.gsqlparser.resolver2.model.ScopeChild;
008import gudusoft.gsqlparser.resolver2.namespace.INamespace;
009import gudusoft.gsqlparser.resolver2.scope.IScope;
010import gudusoft.gsqlparser.resolver2.scope.SelectScope;
011import gudusoft.gsqlparser.resolver2.scope.FromScope;
012import gudusoft.gsqlparser.resolver2.scope.CTEScope;
013import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
014import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
015import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
016
017import java.util.*;
018
019/**
020 * Resolves star columns by pushing down column references from outer queries.
021 *
022 * <p>This is the GSP Java implementation of the star column push-down mechanism.
023 * When an outer query references columns from a CTE or subquery that uses SELECT *,
024 * we need to "push down" those column references to infer which columns are needed.
025 *
026 * <p>Example scenario:
027 * <pre>
028 * WITH my_cte AS (SELECT * FROM Employees)
029 * SELECT id, name FROM my_cte;
030 * </pre>
031 *
032 * <p>Push-down process:
033 * 1. my_cte has SELECT * (star column)
034 * 2. Outer query references id, name from my_cte
035 * 3. Push down: id and name are "required" columns from the star
036 * 4. These columns are inferred to come from Employees table
037 *
038 * <p>Key features:
039 * - Works with ScopeBuildResult from ScopeBuilder
040 * - Multi-level push-down (nested CTEs/subqueries)
041 * - Iterative resolution (may need multiple passes)
042 * - Confidence scoring based on reference type
043 */
044public class StarPushDownResolver {
045
046    /**
047     * Represents a column reference that needs to be pushed down.
048     */
049    public static class PushDownRequest {
050        /** The column name being referenced */
051        public final String columnName;
052
053        /** The column reference node */
054        public final TObjectName columnReference;
055
056        /** The namespace containing the star column */
057        public final INamespace starNamespace;
058
059        /** The star column (TResultColumn with *) */
060        public final TResultColumn starColumn;
061
062        /** Confidence that this push-down is correct */
063        public final double confidence;
064
065        /** Evidence for this push-down */
066        public final String evidence;
067
068        public PushDownRequest(
069                String columnName,
070                TObjectName columnReference,
071                INamespace starNamespace,
072                TResultColumn starColumn,
073                double confidence,
074                String evidence) {
075            this.columnName = columnName;
076            this.columnReference = columnReference;
077            this.starNamespace = starNamespace;
078            this.starColumn = starColumn;
079            this.confidence = confidence;
080            this.evidence = evidence;
081        }
082
083        @Override
084        public String toString() {
085            return String.format("PushDown[%s -> %s, conf=%.2f]",
086                columnName, starNamespace.getDisplayName(), confidence);
087        }
088    }
089
090    /**
091     * Represents the result of push-down resolution.
092     */
093    public static class PushDownResult {
094        /** Namespace -> list of columns pushed down to it */
095        private final Map<INamespace, Set<String>> pushedDownColumns = new LinkedHashMap<>();
096
097        /** Namespace -> list of column references from outer queries */
098        private final Map<INamespace, List<TObjectName>> upLevelReferences = new LinkedHashMap<>();
099
100        /** All processed requests */
101        private final List<PushDownRequest> processedRequests = new ArrayList<>();
102
103        /** Number of iterations performed */
104        private int iterations = 0;
105
106        public void addPushedDownColumn(INamespace namespace, String columnName) {
107            pushedDownColumns.computeIfAbsent(namespace, k -> new LinkedHashSet<>()).add(columnName);
108        }
109
110        public void addUpLevelReference(INamespace namespace, TObjectName reference) {
111            upLevelReferences.computeIfAbsent(namespace, k -> new ArrayList<>()).add(reference);
112        }
113
114        public void addProcessedRequest(PushDownRequest request) {
115            processedRequests.add(request);
116        }
117
118        public Set<String> getPushedDownColumns(INamespace namespace) {
119            return pushedDownColumns.getOrDefault(namespace, Collections.emptySet());
120        }
121
122        public List<TObjectName> getUpLevelReferences(INamespace namespace) {
123            return upLevelReferences.getOrDefault(namespace, Collections.emptyList());
124        }
125
126        public int getTotalPushedDownColumns() {
127            int count = 0;
128            for (Set<String> columns : pushedDownColumns.values()) {
129                count += columns.size();
130            }
131            return count;
132        }
133
134        public int getStarNamespaceCount() {
135            return pushedDownColumns.size();
136        }
137
138        public int getIterations() {
139            return iterations;
140        }
141
142        void setIterations(int iterations) {
143            this.iterations = iterations;
144        }
145
146        public List<PushDownRequest> getProcessedRequests() {
147            return Collections.unmodifiableList(processedRequests);
148        }
149
150        @Override
151        public String toString() {
152            return String.format(
153                "PushDownResult[namespaces=%d, columns=%d, iterations=%d]",
154                pushedDownColumns.size(),
155                getTotalPushedDownColumns(),
156                iterations
157            );
158        }
159    }
160
161    /** Maximum number of iterations for push-down resolution */
162    private final int maxIterations;
163
164    /** Whether to enable debug logging */
165    private final boolean debugLogging;
166
167    /** Name matcher for column comparison */
168    private final INameMatcher nameMatcher;
169
170    public StarPushDownResolver() {
171        this(10, false, new DefaultNameMatcher());
172    }
173
174    public StarPushDownResolver(int maxIterations, boolean debugLogging) {
175        this(maxIterations, debugLogging, new DefaultNameMatcher());
176    }
177
178    public StarPushDownResolver(int maxIterations, boolean debugLogging, INameMatcher nameMatcher) {
179        this.maxIterations = maxIterations;
180        this.debugLogging = debugLogging;
181        this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher();
182    }
183
184    /**
185     * Resolve star columns by pushing down column references.
186     *
187     * @param scopeBuildResult the scope build result from ScopeBuilder
188     * @return push-down resolution result
189     */
190    public PushDownResult resolve(ScopeBuildResult scopeBuildResult) {
191        PushDownResult result = new PushDownResult();
192
193        if (scopeBuildResult == null || scopeBuildResult.getGlobalScope() == null) {
194            return result;
195        }
196
197        // Step 1: Collect all namespaces with star columns
198        List<StarNamespaceInfo> starNamespaces = collectStarNamespaces(scopeBuildResult);
199        log("Found " + starNamespaces.size() + " namespaces with star columns");
200
201        if (starNamespaces.isEmpty()) {
202            return result;
203        }
204
205        // Step 2: Iteratively collect push-down requests and process them
206        int iteration = 0;
207        int newColumnsFound;
208
209        do {
210            newColumnsFound = 0;
211            iteration++;
212
213            // Collect all column references that target star namespaces
214            List<PushDownRequest> requests = collectPushDownRequests(
215                scopeBuildResult, starNamespaces, result);
216
217            log("Iteration " + iteration + ": Found " + requests.size() + " push-down requests");
218
219            // Process each request
220            for (PushDownRequest request : requests) {
221                if (processPushDownRequest(request, result)) {
222                    newColumnsFound++;
223                }
224            }
225
226            log("Iteration " + iteration + ": Added " + newColumnsFound + " new columns");
227
228        } while (newColumnsFound > 0 && iteration < maxIterations);
229
230        result.setIterations(iteration);
231
232        // Step 3: Apply inferred columns to dynamic namespaces
233        applyInferredColumns(result);
234
235        log("Push-down complete: " + result);
236        return result;
237    }
238
239    /**
240     * Information about a namespace with star column.
241     */
242    private static class StarNamespaceInfo {
243        final INamespace namespace;
244        final TResultColumn starColumn;
245        final IScope containingScope;
246        final String alias;
247
248        StarNamespaceInfo(INamespace namespace, TResultColumn starColumn,
249                         IScope containingScope, String alias) {
250            this.namespace = namespace;
251            this.starColumn = starColumn;
252            this.containingScope = containingScope;
253            this.alias = alias;
254        }
255    }
256
257    /**
258     * Collect all namespaces that have star columns.
259     */
260    private List<StarNamespaceInfo> collectStarNamespaces(ScopeBuildResult scopeBuildResult) {
261        List<StarNamespaceInfo> result = new ArrayList<>();
262
263        // Traverse all scopes
264        collectStarNamespacesFromScope(scopeBuildResult.getGlobalScope(), result);
265
266        // Also check SelectScopes from statementScopeMap
267        for (SelectScope selectScope : scopeBuildResult.getStatementScopeMap().values()) {
268            if (selectScope.getFromScope() != null) {
269                collectStarNamespacesFromFromScope(selectScope.getFromScope(), selectScope, result);
270            }
271        }
272
273        return result;
274    }
275
276    /**
277     * Recursively collect star namespaces from a scope.
278     */
279    private void collectStarNamespacesFromScope(IScope scope, List<StarNamespaceInfo> result) {
280        if (scope == null) {
281            return;
282        }
283
284        // Check children of this scope
285        for (ScopeChild child : scope.getChildren()) {
286            INamespace ns = child.getNamespace();
287            if (ns != null && ns.hasStarColumn()) {
288                TResultColumn starCol = findStarColumn(ns);
289                result.add(new StarNamespaceInfo(ns, starCol, scope, child.getAlias()));
290            }
291        }
292
293        // Recurse into child scopes
294        if (scope instanceof SelectScope) {
295            SelectScope selectScope = (SelectScope) scope;
296            if (selectScope.getFromScope() != null) {
297                collectStarNamespacesFromScope(selectScope.getFromScope(), result);
298            }
299        }
300    }
301
302    /**
303     * Collect star namespaces from a FromScope.
304     */
305    private void collectStarNamespacesFromFromScope(FromScope fromScope,
306                                                     SelectScope parentSelect,
307                                                     List<StarNamespaceInfo> result) {
308        for (ScopeChild child : fromScope.getChildren()) {
309            INamespace ns = child.getNamespace();
310            if (ns != null && ns.hasStarColumn()) {
311                // Avoid duplicates
312                boolean exists = result.stream()
313                    .anyMatch(info -> info.namespace == ns);
314                if (!exists) {
315                    TResultColumn starCol = findStarColumn(ns);
316                    result.add(new StarNamespaceInfo(ns, starCol, parentSelect, child.getAlias()));
317                }
318            }
319        }
320    }
321
322    /**
323     * Find the star column in a namespace's SELECT statement.
324     */
325    private TResultColumn findStarColumn(INamespace namespace) {
326        TSelectSqlStatement select = namespace.getSelectStatement();
327        if (select == null || select.getResultColumnList() == null) {
328            return null;
329        }
330
331        TResultColumnList resultList = select.getResultColumnList();
332        for (int i = 0; i < resultList.size(); i++) {
333            TResultColumn rc = resultList.getResultColumn(i);
334            if (rc != null && rc.toString().endsWith("*")) {
335                return rc;
336            }
337        }
338        return null;
339    }
340
341    /**
342     * Collect push-down requests by finding column references that target star namespaces.
343     */
344    private List<PushDownRequest> collectPushDownRequests(
345            ScopeBuildResult scopeBuildResult,
346            List<StarNamespaceInfo> starNamespaces,
347            PushDownResult currentResult) {
348
349        List<PushDownRequest> requests = new ArrayList<>();
350
351        // For each column reference in the build result
352        for (TObjectName colRef : scopeBuildResult.getAllColumnReferences()) {
353            // Get the scope where this column appears
354            IScope colScope = scopeBuildResult.getScopeForColumn(colRef);
355            if (colScope == null) {
356                continue;
357            }
358
359            // Extract table prefix and column name
360            String prefix = getTablePrefix(colRef);
361            String columnName = colRef.getColumnNameOnly();
362
363            if (columnName == null || columnName.isEmpty() || columnName.equals("*")) {
364                continue;  // Skip star columns themselves
365            }
366
367            // Check if this column reference targets any star namespace
368            for (StarNamespaceInfo starInfo : starNamespaces) {
369                if (matchesStarNamespace(colRef, prefix, starInfo, colScope)) {
370                    // Check if already processed
371                    Set<String> existing = currentResult.getPushedDownColumns(starInfo.namespace);
372                    boolean alreadyExists = existing.stream()
373                        .anyMatch(c -> nameMatcher.matches(c, columnName));
374
375                    if (!alreadyExists) {
376                        double confidence = calculateConfidence(colRef, prefix, starInfo);
377                        String evidence = buildEvidence(colRef, starInfo);
378
379                        requests.add(new PushDownRequest(
380                            columnName,
381                            colRef,
382                            starInfo.namespace,
383                            starInfo.starColumn,
384                            confidence,
385                            evidence
386                        ));
387                    }
388                }
389            }
390        }
391
392        return requests;
393    }
394
395    /**
396     * Get the table prefix from a column reference (e.g., "t" from "t.col").
397     */
398    private String getTablePrefix(TObjectName colRef) {
399        if (colRef == null) {
400            return null;
401        }
402
403        // Try to get the table/schema part
404        String fullName = colRef.toString();
405        String columnOnly = colRef.getColumnNameOnly();
406
407        if (fullName != null && columnOnly != null && fullName.contains(".")) {
408            int lastDot = fullName.lastIndexOf('.');
409            if (lastDot > 0) {
410                return fullName.substring(0, lastDot);
411            }
412        }
413
414        return null;
415    }
416
417    /**
418     * Check if a column reference matches a star namespace.
419     */
420    private boolean matchesStarNamespace(TObjectName colRef, String prefix,
421                                         StarNamespaceInfo starInfo, IScope colScope) {
422        // If column has a prefix, it must match the namespace alias/name
423        if (prefix != null && !prefix.isEmpty()) {
424            String nsName = starInfo.alias;
425            if (nsName == null) {
426                nsName = starInfo.namespace.getDisplayName();
427            }
428
429            // Check if prefix matches namespace name (case-insensitive)
430            if (!nameMatcher.matches(prefix, nsName)) {
431                // Also check without schema prefix (e.g., "schema.table" vs "table")
432                if (prefix.contains(".")) {
433                    String shortPrefix = prefix.substring(prefix.lastIndexOf('.') + 1);
434                    if (!nameMatcher.matches(shortPrefix, nsName)) {
435                        return false;
436                    }
437                } else {
438                    return false;
439                }
440            }
441        }
442
443        // Verify scope relationship: column's scope should be able to "see" the star namespace
444        return canAccessNamespace(colScope, starInfo);
445    }
446
447    /**
448     * Check if a scope can access a star namespace.
449     */
450    private boolean canAccessNamespace(IScope colScope, StarNamespaceInfo starInfo) {
451        // Walk up the scope chain from colScope
452        IScope current = colScope;
453        while (current != null) {
454            // Check if this scope contains the star namespace
455            if (current == starInfo.containingScope) {
456                return true;
457            }
458
459            // Check if this is a SelectScope with FromScope containing the namespace
460            if (current instanceof SelectScope) {
461                SelectScope selectScope = (SelectScope) current;
462                if (selectScope.getFromScope() != null) {
463                    for (ScopeChild child : selectScope.getFromScope().getChildren()) {
464                        if (child.getNamespace() == starInfo.namespace) {
465                            return true;
466                        }
467                    }
468                }
469            }
470
471            // Check CTE scopes
472            if (current instanceof CTEScope) {
473                CTEScope cteScope = (CTEScope) current;
474                // CTEs are accessible from this scope and below
475                for (ScopeChild child : cteScope.getChildren()) {
476                    if (child.getNamespace() == starInfo.namespace) {
477                        return true;
478                    }
479                }
480            }
481
482            current = current.getParent();
483        }
484
485        return false;
486    }
487
488    /**
489     * Calculate confidence score for a push-down request.
490     */
491    private double calculateConfidence(TObjectName colRef, String prefix, StarNamespaceInfo starInfo) {
492        // Higher confidence for qualified references (t.col)
493        if (prefix != null && !prefix.isEmpty()) {
494            return 0.95;
495        }
496
497        // Lower confidence for unqualified references
498        return 0.7;
499    }
500
501    /**
502     * Build evidence string for a push-down request.
503     */
504    private String buildEvidence(TObjectName colRef, StarNamespaceInfo starInfo) {
505        return String.format("Column '%s' referenced from outer query, targets '%s' with SELECT *",
506            colRef.toString(), starInfo.namespace.getDisplayName());
507    }
508
509    /**
510     * Process a single push-down request.
511     *
512     * @param request the push-down request
513     * @param result the result to update
514     * @return true if a new column was added, false otherwise
515     */
516    private boolean processPushDownRequest(PushDownRequest request, PushDownResult result) {
517        if (request == null || request.starNamespace == null) {
518            return false;
519        }
520
521        Set<String> existingColumns = result.getPushedDownColumns(request.starNamespace);
522
523        // Check if this column is already pushed down (case-insensitive)
524        for (String existing : existingColumns) {
525            if (nameMatcher.matches(existing, request.columnName)) {
526                return false;  // Already have this column
527            }
528        }
529
530        // Add new pushed-down column
531        result.addPushedDownColumn(request.starNamespace, request.columnName);
532        result.addUpLevelReference(request.starNamespace, request.columnReference);
533        result.addProcessedRequest(request);
534
535        log("Pushed down: " + request.columnName + " -> " + request.starNamespace.getDisplayName());
536
537        return true;
538    }
539
540    /**
541     * Apply inferred columns to dynamic namespaces.
542     */
543    private void applyInferredColumns(PushDownResult result) {
544        for (Map.Entry<INamespace, Set<String>> entry : result.pushedDownColumns.entrySet()) {
545            INamespace namespace = entry.getKey();
546            Set<String> columns = entry.getValue();
547
548            // If namespace supports dynamic inference, add inferred columns
549            if (namespace.supportsDynamicInference()) {
550                for (String columnName : columns) {
551                    namespace.addInferredColumn(columnName, 0.9, "star_push_down");
552                }
553                log("Applied " + columns.size() + " inferred columns to " + namespace.getDisplayName());
554            }
555        }
556    }
557
558    /**
559     * Get statistics about push-down resolution.
560     */
561    public Map<String, Object> getStatistics(PushDownResult result) {
562        Map<String, Object> stats = new LinkedHashMap<>();
563        stats.put("star_namespaces", result.getStarNamespaceCount());
564        stats.put("total_pushed_down", result.getTotalPushedDownColumns());
565        stats.put("iterations", result.getIterations());
566        stats.put("requests_processed", result.getProcessedRequests().size());
567        return stats;
568    }
569
570    private void log(String message) {
571        if (debugLogging) {
572            System.out.println("[StarPushDownResolver] " + message);
573        }
574    }
575}