001package gudusoft.gsqlparser.resolver2.scope;
002
003import gudusoft.gsqlparser.nodes.TCTE;
004import gudusoft.gsqlparser.nodes.TCTEList;
005import gudusoft.gsqlparser.resolver2.ScopeType;
006import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
008import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
009import gudusoft.gsqlparser.resolver2.namespace.CTENamespace;
010import gudusoft.gsqlparser.resolver2.namespace.INamespace;
011import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
012
013import java.util.ArrayList;
014import java.util.LinkedHashMap;
015import java.util.List;
016import java.util.Map;
017
018/**
019 * Scope for Common Table Expressions (WITH clause).
020 * Manages visibility of CTEs defined in a WITH clause.
021 *
022 * <p>Example:
023 * <pre>
024 * WITH
025 *   cte1 AS (SELECT id, name FROM users),
026 *   cte2 AS (SELECT * FROM cte1 WHERE id > 100)  -- can reference cte1
027 * SELECT * FROM cte2;  -- can reference both cte1 and cte2
028 * </pre>
029 *
030 * <p>Key features:
031 * - Multiple CTEs in same WITH clause
032 * - CTEs can reference earlier CTEs in the same WITH clause
033 * - Recursive CTE support (same CTE references itself)
034 * - CTEs are visible to the main query and to later CTEs
035 */
036public class CTEScope extends ListBasedScope {
037
038    /**
039     * Map of CTE name to CTENamespace.
040     * Preserves insertion order to handle forward references correctly.
041     */
042    private final Map<String, CTENamespace> cteMap;
043
044    /**
045     * The WITH clause CTE list node
046     */
047    private final TCTEList cteList;
048
049    public CTEScope(IScope parent, TCTEList cteList) {
050        super(parent, cteList, ScopeType.CTE);
051        this.cteList = cteList;
052        this.cteMap = new LinkedHashMap<>();
053    }
054
055    /**
056     * Add a CTE to this scope.
057     * CTEs are added in order, allowing later CTEs to reference earlier ones.
058     *
059     * @param cteName the CTE name (alias)
060     * @param cteNamespace the CTE namespace
061     */
062    public void addCTE(String cteName, CTENamespace cteNamespace) {
063        if (cteName == null || cteNamespace == null) {
064            return;
065        }
066
067        // Store in map for name resolution
068        cteMap.put(cteName, cteNamespace);
069
070        // Add as child to ListBasedScope (makes it visible to parent queries)
071        // CTEs are always non-nullable
072        addChild(cteNamespace, cteName, false);
073    }
074
075    /**
076     * Check if a CTE with the given name exists in this scope.
077     *
078     * <p>Cleanup follow-up to S15: route compare through {@link INameMatcher}
079     * (sourced from the {@link GlobalScope} at the root of the scope chain)
080     * so per-dialect quoted-vs-unquoted rules apply. CTEs are table-like
081     * references, so when the matcher is a {@link VendorNameMatcher}, route
082     * through {@link ESQLDataObjectType#dotTable} explicitly because BigQuery
083     * and MySQL have divergent table-vs-column rules.</p>
084     *
085     * @param cteName the CTE name to check
086     * @return true if CTE exists, false otherwise
087     */
088    public boolean hasCTE(String cteName) {
089        if (cteName == null) {
090            return false;
091        }
092
093        INameMatcher matcher = findNameMatcher();
094        for (String existingName : cteMap.keySet()) {
095            if (cteNameMatches(matcher, existingName, cteName)) {
096                return true;
097            }
098        }
099
100        return false;
101    }
102
103    /**
104     * Get a CTE namespace by name.
105     *
106     * <p>Cleanup follow-up to S15: see {@link #hasCTE(String)} javadoc for
107     * the matcher-routing rationale.</p>
108     *
109     * @param cteName the CTE name
110     * @return the CTENamespace, or null if not found
111     */
112    public CTENamespace getCTE(String cteName) {
113        if (cteName == null) {
114            return null;
115        }
116
117        INameMatcher matcher = findNameMatcher();
118        for (Map.Entry<String, CTENamespace> entry : cteMap.entrySet()) {
119            String existingName = entry.getKey();
120            if (cteNameMatches(matcher, existingName, cteName)) {
121                return entry.getValue();
122            }
123        }
124
125        return null;
126    }
127
128    private static boolean cteNameMatches(INameMatcher matcher, String existingName, String cteName) {
129        if (matcher instanceof VendorNameMatcher) {
130            return ((VendorNameMatcher) matcher).matches(existingName, cteName, ESQLDataObjectType.dotTable);
131        }
132        return matcher.matches(existingName, cteName);
133    }
134
135    /**
136     * Walk the parent chain to find the {@link GlobalScope}'s name matcher.
137     * Falls back to {@link DefaultNameMatcher} for synthetic-scope unit tests
138     * that do not include a {@code GlobalScope} in the chain.
139     */
140    private INameMatcher findNameMatcher() {
141        IScope cursor = this;
142        while (cursor != null && !(cursor instanceof EmptyScope)) {
143            if (cursor instanceof GlobalScope) {
144                INameMatcher m = ((GlobalScope) cursor).getNameMatcher();
145                if (m != null) return m;
146                break;
147            }
148            IScope next = cursor.getParent();
149            if (next == cursor) break;
150            cursor = next;
151        }
152        return new DefaultNameMatcher();
153    }
154
155    /**
156     * Get all CTEs in this scope.
157     *
158     * @return list of all CTE namespaces in definition order
159     */
160    public List<CTENamespace> getAllCTEs() {
161        return new ArrayList<>(cteMap.values());
162    }
163
164    /**
165     * Get the number of CTEs in this scope.
166     *
167     * @return CTE count
168     */
169    public int getCTECount() {
170        return cteMap.size();
171    }
172
173    /**
174     * Resolve a table name, checking CTEs first before delegating to parent.
175     * This allows CTEs to shadow real tables.
176     *
177     * @param tableName the table name to resolve
178     * @return the namespace for the CTE or table, or null if not found
179     */
180    @Override
181    public INamespace resolveTable(String tableName) {
182        // First check if it's a CTE in this scope
183        CTENamespace cte = getCTE(tableName);
184        if (cte != null) {
185            return cte;
186        }
187
188        // Delegate to parent scope (might be a real table)
189        return super.resolveTable(tableName);
190    }
191
192    /**
193     * Check if this scope contains a recursive CTE.
194     * A recursive CTE is one that references itself in its definition.
195     *
196     * @return true if any CTE in this scope is recursive
197     */
198    public boolean hasRecursiveCTE() {
199        for (CTENamespace cte : cteMap.values()) {
200            if (cte.isRecursive()) {
201                return true;
202            }
203        }
204        return false;
205    }
206
207    /**
208     * Get all recursive CTEs in this scope.
209     *
210     * @return list of recursive CTE namespaces
211     */
212    public List<CTENamespace> getRecursiveCTEs() {
213        List<CTENamespace> recursive = new ArrayList<>();
214        for (CTENamespace cte : cteMap.values()) {
215            if (cte.isRecursive()) {
216                recursive.add(cte);
217            }
218        }
219        return recursive;
220    }
221
222    public TCTEList getCTEList() {
223        return cteList;
224    }
225
226    @Override
227    public String toString() {
228        StringBuilder sb = new StringBuilder("CTEScope(");
229        sb.append("count=").append(cteMap.size());
230
231        if (!cteMap.isEmpty()) {
232            sb.append(", ctes=[");
233            boolean first = true;
234            for (String cteName : cteMap.keySet()) {
235                if (!first) {
236                    sb.append(", ");
237                }
238                sb.append(cteName);
239                first = false;
240            }
241            sb.append("]");
242        }
243
244        if (hasRecursiveCTE()) {
245            sb.append(", recursive=true");
246        }
247
248        sb.append(")");
249        return sb.toString();
250    }
251}