001package gudusoft.gsqlparser.resolver2.scope; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.nodes.TParseTreeNode; 005import gudusoft.gsqlparser.compiler.TContext; 006import gudusoft.gsqlparser.resolver2.ScopeType; 007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 008import gudusoft.gsqlparser.resolver2.model.QualifiedName; 009import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver; 010import gudusoft.gsqlparser.resolver2.namespace.INamespace; 011import gudusoft.gsqlparser.sqlenv.TSQLEnv; 012 013import java.util.*; 014 015/** 016 * Global scope - contains session-level objects and metadata. 017 * This is typically the root of the scope tree. 018 * 019 * <p>Responsibilities: 020 * <ol> 021 * <li>Access to global metadata (SQLEnv)</li> 022 * <li>Database/schema context with proper qualified name resolution</li> 023 * <li>Session variables and settings</li> 024 * </ol> 025 * 026 * <h3>Qualified Name Resolution</h3> 027 * <p>When resolving table names, this scope uses {@link QualifiedNameResolver} 028 * to properly handle partial names (e.g., just "table" or "schema.table") 029 * by applying default catalog/schema from {@link TSQLEnv}. 030 * 031 * <p>Example: With default catalog="mydb" and schema="dbo": 032 * <ul> 033 * <li>"users" matches "mydb.dbo.users"</li> 034 * <li>"hr.employees" matches "mydb.hr.employees"</li> 035 * <li>"otherdb.hr.employees" matches exactly</li> 036 * </ul> 037 * 038 * @see QualifiedNameResolver 039 * @see QualifiedName 040 */ 041public class GlobalScope extends AbstractScope { 042 043 /** Global context from parser */ 044 private final TContext globalContext; 045 046 /** Name matcher for this session */ 047 private final INameMatcher nameMatcher; 048 049 /** SQL environment containing default catalog/schema */ 050 private TSQLEnv sqlEnv; 051 052 /** Database vendor for qualified name parsing */ 053 private EDbVendor vendor; 054 055 /** Qualified name resolver for normalizing table references */ 056 private QualifiedNameResolver qualifiedNameResolver; 057 058 /** Schema-qualified tables available globally, keyed by normalized qualified name */ 059 private final Map<String, INamespace> globalTables = new HashMap<>(); 060 061 /** Maps normalized qualified names to their QualifiedName objects for matching */ 062 private final Map<String, QualifiedName> globalTableQualifiedNames = new HashMap<>(); 063 064 public GlobalScope(TContext globalContext, INameMatcher nameMatcher) { 065 super(EmptyScope.INSTANCE, null, ScopeType.GLOBAL); 066 this.globalContext = globalContext; 067 this.nameMatcher = nameMatcher; 068 this.vendor = EDbVendor.dbvoracle; // Default 069 } 070 071 /** 072 * Create a GlobalScope with SQL environment for qualified name resolution. 073 * 074 * @param globalContext The global parser context 075 * @param nameMatcher The name matcher for case sensitivity 076 * @param sqlEnv The SQL environment with default catalog/schema 077 * @param vendor The database vendor 078 */ 079 public GlobalScope(TContext globalContext, INameMatcher nameMatcher, 080 TSQLEnv sqlEnv, EDbVendor vendor) { 081 super(EmptyScope.INSTANCE, null, ScopeType.GLOBAL); 082 this.globalContext = globalContext; 083 this.nameMatcher = nameMatcher; 084 this.sqlEnv = sqlEnv; 085 this.vendor = vendor; 086 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 087 } 088 089 /** 090 * Set the SQL environment for qualified name resolution. 091 * 092 * @param sqlEnv The SQL environment with default catalog/schema 093 */ 094 public void setSqlEnv(TSQLEnv sqlEnv) { 095 this.sqlEnv = sqlEnv; 096 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 097 } 098 099 /** 100 * Get the SQL environment. 101 */ 102 public TSQLEnv getSqlEnv() { 103 return sqlEnv; 104 } 105 106 /** 107 * Set the database vendor. 108 */ 109 public void setVendor(EDbVendor vendor) { 110 this.vendor = vendor; 111 if (sqlEnv != null) { 112 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 113 } 114 } 115 116 /** 117 * Get the database vendor. 118 */ 119 public EDbVendor getVendor() { 120 return vendor; 121 } 122 123 /** 124 * Add a globally accessible table (from metadata). 125 * 126 * <p>The qualified name should be in the format "catalog.schema.table" 127 * or "schema.table" or just "table". The name will be parsed and stored 128 * for proper matching later. 129 * 130 * @param qualifiedNameStr The qualified table name string 131 * @param tableNamespace The namespace representing the table 132 */ 133 public void addGlobalTable(String qualifiedNameStr, INamespace tableNamespace) { 134 globalTables.put(qualifiedNameStr, tableNamespace); 135 136 // Parse the qualified name string for proper matching 137 QualifiedName qName = parseQualifiedNameString(qualifiedNameStr); 138 if (qName != null) { 139 globalTableQualifiedNames.put(qualifiedNameStr, qName); 140 } 141 } 142 143 /** 144 * Add a globally accessible table with explicit catalog/schema/name. 145 * 146 * @param catalog The catalog name (nullable) 147 * @param schema The schema name (nullable) 148 * @param tableName The table name (required) 149 * @param tableNamespace The namespace representing the table 150 */ 151 public void addGlobalTable(String catalog, String schema, String tableName, 152 INamespace tableNamespace) { 153 QualifiedName qName = QualifiedName.forTable(catalog, schema, tableName); 154 String key = qName.toString(); 155 globalTables.put(key, tableNamespace); 156 globalTableQualifiedNames.put(key, qName); 157 } 158 159 /** 160 * Parse a qualified name string into a QualifiedName object. 161 */ 162 private QualifiedName parseQualifiedNameString(String qualifiedNameStr) { 163 if (qualifiedNameStr == null || qualifiedNameStr.isEmpty()) { 164 return null; 165 } 166 167 // Use QualifiedNameResolver if available 168 if (qualifiedNameResolver != null) { 169 return qualifiedNameResolver.resolve(qualifiedNameStr); 170 } 171 172 // Fallback: simple dot-split parsing 173 String[] parts = qualifiedNameStr.split("\\."); 174 if (parts.length == 1) { 175 return QualifiedName.forTable(parts[0]); 176 } else if (parts.length == 2) { 177 return QualifiedName.forTable(parts[0], parts[1]); 178 } else if (parts.length >= 3) { 179 return QualifiedName.forTable(parts[0], parts[1], parts[2]); 180 } 181 return null; 182 } 183 184 @Override 185 public INamespace resolveTable(String tableName) { 186 if (tableName == null || tableName.isEmpty()) { 187 return super.resolveTable(tableName); 188 } 189 190 // Parse the input table name into a QualifiedName 191 QualifiedName inputQName = parseQualifiedNameString(tableName); 192 if (inputQName == null) { 193 return super.resolveTable(tableName); 194 } 195 196 // Apply defaults to get the fully qualified search name 197 String defaultCatalog = getDefaultCatalog(); 198 String defaultSchema = getDefaultSchema(); 199 QualifiedName normalizedInput = inputQName.withDefaults(defaultCatalog, defaultSchema); 200 201 // Look for matching table in global tables 202 for (Map.Entry<String, QualifiedName> entry : globalTableQualifiedNames.entrySet()) { 203 String key = entry.getKey(); 204 QualifiedName storedQName = entry.getValue(); 205 206 // Apply defaults to stored name for comparison 207 QualifiedName normalizedStored = storedQName.withDefaults(defaultCatalog, defaultSchema); 208 209 // Check for match using proper qualified name comparison 210 if (matchesQualifiedName(normalizedInput, normalizedStored)) { 211 return globalTables.get(key); 212 } 213 } 214 215 // Fallback: legacy matching for backward compatibility 216 for (Map.Entry<String, INamespace> entry : globalTables.entrySet()) { 217 String qualifiedName = entry.getKey(); 218 if (nameMatcher.matches(qualifiedName, tableName)) { 219 return entry.getValue(); 220 } 221 } 222 223 // Not found in global scope 224 return super.resolveTable(tableName); 225 } 226 227 /** 228 * Check if two qualified names match. 229 * 230 * <p>Matching rules: 231 * <ul> 232 * <li>Table names must match (case-insensitive by default)</li> 233 * <li>If both have schemas, schemas must match</li> 234 * <li>If both have catalogs, catalogs must match</li> 235 * </ul> 236 */ 237 private boolean matchesQualifiedName(QualifiedName input, QualifiedName stored) { 238 // Table names must match 239 String inputName = input.getTableName(); 240 String storedName = stored.getTableName(); 241 if (inputName == null || storedName == null) { 242 return false; 243 } 244 if (!nameMatcher.matches(inputName, storedName)) { 245 return false; 246 } 247 248 // If both have schemas, they must match 249 String inputSchema = input.getSchema(); 250 String storedSchema = stored.getSchema(); 251 if (inputSchema != null && storedSchema != null) { 252 if (!nameMatcher.matches(inputSchema, storedSchema)) { 253 return false; 254 } 255 } 256 257 // If both have catalogs, they must match 258 String inputCatalog = input.getCatalog(); 259 String storedCatalog = stored.getCatalog(); 260 if (inputCatalog != null && storedCatalog != null) { 261 if (!nameMatcher.matches(inputCatalog, storedCatalog)) { 262 return false; 263 } 264 } 265 266 return true; 267 } 268 269 /** 270 * Get the default catalog from SQL environment. 271 */ 272 public String getDefaultCatalog() { 273 if (sqlEnv != null) { 274 return sqlEnv.getDefaultCatalogName(); 275 } 276 return null; 277 } 278 279 /** 280 * Get the default schema from SQL environment. 281 */ 282 public String getDefaultSchema() { 283 if (sqlEnv != null) { 284 return sqlEnv.getDefaultSchemaName(); 285 } 286 return null; 287 } 288 289 @Override 290 public void resolve(List<String> names, 291 INameMatcher matcher, 292 boolean deep, 293 IResolved resolved) { 294 if (names == null || names.isEmpty()) { 295 super.resolve(names, matcher, deep, resolved); 296 return; 297 } 298 299 // Build a qualified name from the parts 300 String tableName; 301 if (names.size() == 1) { 302 tableName = names.get(0); 303 } else if (names.size() == 2) { 304 tableName = names.get(0) + "." + names.get(1); 305 } else { 306 tableName = names.get(0) + "." + names.get(1) + "." + names.get(2); 307 } 308 309 // Try to resolve as a table 310 INamespace tableNs = resolveTable(tableName); 311 if (tableNs != null) { 312 // Report found with proper parameters 313 resolved.found(tableNs, false, this, null, java.util.Collections.emptyList()); 314 return; 315 } 316 317 // Not found - delegate to parent 318 super.resolve(names, matcher, deep, resolved); 319 } 320 321 @Override 322 public List<INamespace> getVisibleNamespaces() { 323 return new ArrayList<>(globalTables.values()); 324 } 325 326 public TContext getGlobalContext() { 327 return globalContext; 328 } 329 330 public INameMatcher getNameMatcher() { 331 return nameMatcher; 332 } 333 334 /** 335 * Get the qualified name resolver. 336 */ 337 public QualifiedNameResolver getQualifiedNameResolver() { 338 return qualifiedNameResolver; 339 } 340 341 @Override 342 public String toString() { 343 StringBuilder sb = new StringBuilder("GlobalScope(tables="); 344 sb.append(globalTables.size()); 345 if (sqlEnv != null) { 346 String defCatalog = getDefaultCatalog(); 347 String defSchema = getDefaultSchema(); 348 if (defCatalog != null || defSchema != null) { 349 sb.append(", defaults="); 350 sb.append(defCatalog != null ? defCatalog : "*"); 351 sb.append("."); 352 sb.append(defSchema != null ? defSchema : "*"); 353 } 354 } 355 sb.append(")"); 356 return sb.toString(); 357 } 358}