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}