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