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}