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}