001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.nodes.TTable; 004import gudusoft.gsqlparser.resolver2.namespace.INamespace; 005import gudusoft.gsqlparser.resolver2.scope.IScope; 006import gudusoft.gsqlparser.resolver2.scope.SelectScope; 007 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011 012/** 013 * Index structure for fast lookups in FromScope. 014 * 015 * <p>This class optimizes O(N) linear scans to O(1) hash lookups for common 016 * operations like finding tables by alias/name and finding candidate namespaces 017 * for star column inference.</p> 018 * 019 * <p>The index is built lazily on first access and cached for subsequent lookups. 020 * This avoids the overhead of building the index when it's not needed.</p> 021 * 022 * <p><b>Performance improvement:</b> For queries with N tables and M column references, 023 * reduces complexity from O(N*M) to O(N + M).</p> 024 * 025 * @since 3.1.0.9 026 */ 027public class FromScopeIndex { 028 029 /** 030 * Map from lowercase alias/name to ScopeChild for O(1) lookup. 031 * Key is always lowercase for case-insensitive matching. 032 */ 033 private final Map<String, ScopeChild> aliasMap; 034 035 /** 036 * Map from lowercase namespace display name to ScopeChild. 037 * Used when lookup by alias fails but table name might match. 038 */ 039 private final Map<String, ScopeChild> nameMap; 040 041 /** 042 * First namespace that supports star columns or dynamic inference. 043 * Used for unqualified column resolution when no table prefix is specified. 044 */ 045 private final INamespace preferredStarNamespace; 046 047 /** 048 * Total number of children in the FromScope. 049 */ 050 private final int childCount; 051 052 /** 053 * Creates an index from the given list of scope children. 054 * 055 * @param children The list of ScopeChild from a FromScope 056 */ 057 public FromScopeIndex(List<ScopeChild> children) { 058 this.aliasMap = new HashMap<>(); 059 this.nameMap = new HashMap<>(); 060 this.childCount = children != null ? children.size() : 0; 061 062 INamespace firstStar = null; 063 064 if (children != null) { 065 for (ScopeChild child : children) { 066 // Index by alias (lowercase for case-insensitive matching) 067 String alias = child.getAlias(); 068 if (alias != null && !alias.isEmpty()) { 069 aliasMap.put(alias.toLowerCase(), child); 070 } 071 072 // Also index by namespace display name 073 INamespace ns = child.getNamespace(); 074 if (ns != null) { 075 String displayName = ns.getDisplayName(); 076 if (displayName != null && !displayName.isEmpty()) { 077 nameMap.put(displayName.toLowerCase(), child); 078 } 079 080 // Track first star/dynamic namespace 081 if (firstStar == null && 082 (ns.hasStarColumn() || ns.supportsDynamicInference())) { 083 firstStar = ns; 084 } 085 } 086 } 087 } 088 089 this.preferredStarNamespace = firstStar; 090 } 091 092 /** 093 * Finds a ScopeChild by its alias (case-insensitive). 094 * 095 * @param alias The alias to search for 096 * @return The matching ScopeChild, or null if not found 097 */ 098 public ScopeChild findByAlias(String alias) { 099 if (alias == null || alias.isEmpty()) { 100 return null; 101 } 102 return aliasMap.get(alias.toLowerCase()); 103 } 104 105 /** 106 * Finds a ScopeChild by alias first, then by namespace name (case-insensitive). 107 * 108 * @param qualifier The qualifier (alias or table name) to search for 109 * @return The matching ScopeChild, or null if not found 110 */ 111 public ScopeChild findByQualifier(String qualifier) { 112 if (qualifier == null || qualifier.isEmpty()) { 113 return null; 114 } 115 116 String lowerQualifier = qualifier.toLowerCase(); 117 118 // First try alias 119 ScopeChild result = aliasMap.get(lowerQualifier); 120 if (result != null) { 121 return result; 122 } 123 124 // Then try namespace name 125 return nameMap.get(lowerQualifier); 126 } 127 128 /** 129 * Gets the TTable from a ScopeChild by qualifier, if the namespace represents a table. 130 * 131 * @param qualifier The qualifier (alias or table name) to search for 132 * @return The TTable if found and the namespace is a table, null otherwise 133 */ 134 public TTable findTableByQualifier(String qualifier) { 135 ScopeChild child = findByAlias(qualifier); 136 if (child != null) { 137 INamespace ns = child.getNamespace(); 138 if (ns != null && ns.getNode() instanceof TTable) { 139 return (TTable) ns.getNode(); 140 } 141 } 142 return null; 143 } 144 145 /** 146 * Gets the first namespace that supports star columns or dynamic inference. 147 * Used for resolving unqualified column references. 148 * 149 * @return The preferred star namespace, or null if none exists 150 */ 151 public INamespace getPreferredStarNamespace() { 152 return preferredStarNamespace; 153 } 154 155 /** 156 * Finds a candidate namespace for column resolution. 157 * 158 * <p>If a table prefix is provided, looks up by that prefix. 159 * Otherwise, returns the first star/dynamic namespace for unqualified columns.</p> 160 * 161 * @param tablePrefix The table prefix (alias or name), or null for unqualified columns 162 * @return The candidate namespace, or null if not found 163 */ 164 public INamespace findCandidateNamespace(String tablePrefix) { 165 if (tablePrefix != null && !tablePrefix.isEmpty()) { 166 // Qualified reference - find by prefix 167 ScopeChild child = findByQualifier(tablePrefix); 168 if (child != null) { 169 return child.getNamespace(); 170 } 171 return null; 172 } else { 173 // Unqualified reference - use star namespace 174 return preferredStarNamespace; 175 } 176 } 177 178 /** 179 * Gets the total number of children indexed. 180 * 181 * @return The child count 182 */ 183 public int getChildCount() { 184 return childCount; 185 } 186 187 /** 188 * Checks if the index is empty. 189 * 190 * @return true if no children are indexed 191 */ 192 public boolean isEmpty() { 193 return childCount == 0; 194 } 195 196 /** 197 * Creates an index from a scope, extracting the FromScope if needed. 198 * 199 * @param scope The scope (SelectScope, UpdateScope, or FromScope) 200 * @return A new FromScopeIndex, or null if no FromScope is found 201 */ 202 public static FromScopeIndex fromScope(IScope scope) { 203 if (scope == null) { 204 return null; 205 } 206 207 IScope fromScope = null; 208 209 if (scope instanceof SelectScope) { 210 fromScope = ((SelectScope) scope).getFromScope(); 211 } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.UpdateScope) { 212 fromScope = ((gudusoft.gsqlparser.resolver2.scope.UpdateScope) scope).getFromScope(); 213 } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.FromScope) { 214 fromScope = scope; 215 } 216 217 if (fromScope == null) { 218 return null; 219 } 220 221 return new FromScopeIndex(fromScope.getChildren()); 222 } 223 224 @Override 225 public String toString() { 226 return String.format("FromScopeIndex(aliases=%d, names=%d, hasStar=%b)", 227 aliasMap.size(), nameMap.size(), preferredStarNamespace != null); 228 } 229}