001package gudusoft.gsqlparser.resolver2.scope; 002 003import gudusoft.gsqlparser.nodes.TParseTreeNode; 004import gudusoft.gsqlparser.resolver2.ScopeType; 005import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher; 006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 007import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher; 008import gudusoft.gsqlparser.resolver2.model.ResolvePath; 009import gudusoft.gsqlparser.resolver2.model.ScopeChild; 010import gudusoft.gsqlparser.resolver2.namespace.INamespace; 011import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 012 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.List; 016 017/** 018 * Base class for scopes that manage a list of child namespaces. 019 * Used for FROM clauses, which contain multiple tables/subqueries. 020 * 021 * Key features: 022 * 1. Maintains ordered list of children 023 * 2. Resolves names by searching through children 024 * 3. Supports aliases 025 */ 026public abstract class ListBasedScope extends AbstractScope { 027 028 /** Child namespaces in this scope */ 029 protected final List<ScopeChild> children = new ArrayList<>(); 030 031 protected ListBasedScope(IScope parent, TParseTreeNode node, ScopeType scopeType) { 032 super(parent, node, scopeType); 033 } 034 035 @Override 036 public void addChild(INamespace namespace, String alias, boolean nullable) { 037 if (alias == null) { 038 throw new IllegalArgumentException("Alias cannot be null in ListBasedScope"); 039 } 040 children.add(new ScopeChild(children.size(), alias, namespace, nullable)); 041 } 042 043 @Override 044 public List<ScopeChild> getChildren() { 045 return Collections.unmodifiableList(children); 046 } 047 048 @Override 049 public INamespace resolveTable(String tableName) { 050 // Search in children only - do NOT delegate to parent 051 // The parent (SelectScope) already handles fallback to outer scopes 052 // Delegating to parent causes infinite recursion: 053 // SelectScope.resolveTable -> fromScope.resolveTable -> parent.resolveTable -> SelectScope.resolveTable -> ... 054 // Slice S15: route alias compare through INameMatcher so per-dialect 055 // quoted-vs-unquoted rules apply (e.g. Oracle quoted alias is 056 // case-sensitive, unquoted folds to upper). The matcher is sourced 057 // from the GlobalScope at the root of the scope chain. When the 058 // matcher is a {@link VendorNameMatcher}, route through {@link 059 // ESQLDataObjectType#dotTable} explicitly because BigQuery and 060 // MySQL have different rules between table and column object types 061 // (BigQuery: tables SENSITIVE, columns INSENSITIVE; MySQL: tables 062 // depend on lower_case_table_names, columns always INSENSITIVE). 063 INameMatcher matcher = findNameMatcher(); 064 for (ScopeChild child : children) { 065 if (aliasMatches(matcher, child.getAlias(), tableName)) { 066 return child.getNamespace(); 067 } 068 } 069 070 // Not found - return null, let caller handle fallback 071 return null; 072 } 073 074 private static boolean aliasMatches(INameMatcher matcher, String childAlias, String tableName) { 075 if (matcher instanceof VendorNameMatcher) { 076 return ((VendorNameMatcher) matcher).matches(childAlias, tableName, ESQLDataObjectType.dotTable); 077 } 078 return matcher.matches(childAlias, tableName); 079 } 080 081 /** 082 * Walk the parent chain to find the {@link GlobalScope}'s name matcher. 083 * Falls back to a {@link DefaultNameMatcher} if the chain does not 084 * include a {@code GlobalScope} (e.g. unit tests with synthetic scopes). 085 */ 086 private INameMatcher findNameMatcher() { 087 IScope cursor = this; 088 while (cursor != null && !(cursor instanceof EmptyScope)) { 089 if (cursor instanceof GlobalScope) { 090 INameMatcher m = ((GlobalScope) cursor).getNameMatcher(); 091 if (m != null) return m; 092 break; 093 } 094 IScope next = cursor.getParent(); 095 if (next == cursor) break; 096 cursor = next; 097 } 098 return new DefaultNameMatcher(); 099 } 100 101 @Override 102 public void resolve(List<String> names, 103 INameMatcher matcher, 104 boolean deep, 105 IResolved resolved) { 106 if (names.isEmpty()) { 107 return; 108 } 109 110 String firstName = names.get(0); 111 112 // Search through children 113 for (ScopeChild child : children) { 114 if (matcher.matches(child.getAlias(), firstName)) { 115 // Found matching child 116 INamespace namespace = child.getNamespace(); 117 ResolvePath path = new ResolvePath(); 118 119 if (names.size() == 1) { 120 // Just the table alias 121 resolved.found(namespace, child.isNullable(), this, path, Collections.emptyList()); 122 } else { 123 // Resolve remaining parts in the namespace 124 List<String> remaining = names.subList(1, names.size()); 125 resolveInNamespace(namespace, child.isNullable(), remaining, matcher, path, resolved); 126 } 127 } 128 } 129 130 // Also try parent scope 131 parent.resolve(names, matcher, deep, resolved); 132 } 133 134 /** 135 * Resolve remaining name parts within a namespace. 136 * For example, if we found table "t" and need to resolve "t.col1", 137 * this method handles resolving "col1" within table t. 138 */ 139 protected void resolveInNamespace(INamespace namespace, 140 boolean nullable, 141 List<String> remainingNames, 142 INameMatcher matcher, 143 ResolvePath path, 144 IResolved resolved) { 145 if (remainingNames.isEmpty()) { 146 resolved.found(namespace, nullable, this, path, Collections.emptyList()); 147 return; 148 } 149 150 // For now, simple implementation - just mark as found with remaining names 151 // Full implementation would traverse into structured types 152 resolved.found(namespace, nullable, this, path, remainingNames); 153 } 154 155 @Override 156 public List<INamespace> getVisibleNamespaces() { 157 List<INamespace> visible = new ArrayList<>(); 158 for (ScopeChild child : children) { 159 visible.add(child.getNamespace()); 160 } 161 // Add parent's visible namespaces 162 visible.addAll(parent.getVisibleNamespaces()); 163 return visible; 164 } 165 166 @Override 167 public String toString() { 168 return String.format("%s(children=%d)", scopeType, children.size()); 169 } 170}