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}