001package gudusoft.gsqlparser.resolver2.context;
002
003import gudusoft.gsqlparser.EDbObjectType;
004import gudusoft.gsqlparser.ESqlStatementType;
005import gudusoft.gsqlparser.TCustomSqlStatement;
006import gudusoft.gsqlparser.TStatementList;
007import gudusoft.gsqlparser.sqlenv.TSQLEnv;
008import gudusoft.gsqlparser.stmt.TSetSchemaStmt;
009import gudusoft.gsqlparser.stmt.TUseDatabase;
010import gudusoft.gsqlparser.stmt.TUseStmt;
011
012/**
013 * Delta 4: Database context tracker for USE/SET statements.
014 *
015 * Tracks USE DATABASE, USE SCHEMA, SET SCHEMA, and similar statements
016 * to maintain the current database/schema context during resolution.
017 *
018 * This enables proper resolution of unqualified table names when the
019 * context is set by prior statements in the same SQL batch.
020 *
021 * Example:
022 * <pre>
023 * USE mydb;
024 * SELECT * FROM t;  -- 't' should resolve to 'mydb.dbo.t' (or 'mydb.t')
025 * </pre>
026 */
027public class DatabaseContextTracker {
028
029    private String currentDatabase;
030    private String currentSchema;
031
032    /**
033     * Process all statements in the list to extract database/schema context.
034     *
035     * @param statements The list of SQL statements to process
036     */
037    public void processStatements(TStatementList statements) {
038        if (statements == null) {
039            return;
040        }
041
042        for (int i = 0; i < statements.size(); i++) {
043            TCustomSqlStatement stmt = statements.get(i);
044            if (stmt != null) {
045                processStatement(stmt);
046            }
047        }
048    }
049
050    /**
051     * Process a single statement and update context if it's a USE/SET statement.
052     *
053     * @param stmt The statement to process
054     */
055    public void processStatement(TCustomSqlStatement stmt) {
056        if (stmt == null) {
057            return;
058        }
059
060        ESqlStatementType stmtType = stmt.sqlstatementtype;
061
062        // Handle USE DATABASE
063        if (stmtType == ESqlStatementType.sstUseDatabase) {
064            processUseDatabase((TUseDatabase) stmt);
065        }
066        // Handle general USE statement (deprecated but still used)
067        else if (stmtType == ESqlStatementType.sstUse) {
068            processUseStmt((TUseStmt) stmt);
069        }
070        // Handle USE SCHEMA (Snowflake, Databricks)
071        else if (stmtType == ESqlStatementType.sstUseSchema) {
072            processUseSchema(stmt);
073        }
074        // Handle USE CATALOG (Databricks)
075        else if (stmtType == ESqlStatementType.sstUseCatalog) {
076            processUseCatalog(stmt);
077        }
078        // Handle SET SCHEMA (Netezza, etc.)
079        else if (stmtType == ESqlStatementType.sstSetSchema) {
080            processSetSchema((TSetSchemaStmt) stmt);
081        }
082    }
083
084    /**
085     * Process USE DATABASE statement.
086     */
087    private void processUseDatabase(TUseDatabase useDb) {
088        if (useDb == null) {
089            return;
090        }
091
092        // Check if it's actually a USE SCHEMA (some vendors use TUseDatabase for both)
093        if (useDb.isSchema() && useDb.getSchemaName() != null) {
094            currentSchema = useDb.getSchemaName().toString();
095        } else if (useDb.getDatabaseName() != null) {
096            currentDatabase = useDb.getDatabaseName().toString();
097        }
098    }
099
100    /**
101     * Process general USE statement.
102     */
103    private void processUseStmt(TUseStmt useStmt) {
104        if (useStmt == null || useStmt.getDbObjectName() == null) {
105            return;
106        }
107
108        EDbObjectType objType = useStmt.getDbObjectType();
109        String name = useStmt.getDbObjectName().toString();
110
111        if (objType == EDbObjectType.database) {
112            currentDatabase = name;
113        } else if (objType == EDbObjectType.schema) {
114            currentSchema = name;
115        }
116    }
117
118    /**
119     * Process USE SCHEMA statement (Snowflake, Databricks).
120     */
121    private void processUseSchema(TCustomSqlStatement stmt) {
122        // USE SCHEMA typically has a schema name in the first token after USE SCHEMA
123        // We need to extract it from the statement text or tokens
124        if (stmt != null && stmt.toString() != null) {
125            String text = stmt.toString().trim();
126            // Pattern: USE SCHEMA schemaName or USE DATABASE schemaName
127            String[] parts = text.split("\\s+");
128            if (parts.length >= 3) {
129                currentSchema = stripQuotes(parts[parts.length - 1]);
130            } else if (parts.length >= 2) {
131                currentSchema = stripQuotes(parts[parts.length - 1]);
132            }
133        }
134    }
135
136    /**
137     * Process USE CATALOG statement (Databricks).
138     */
139    private void processUseCatalog(TCustomSqlStatement stmt) {
140        // USE CATALOG catalogName or SET CATALOG catalogName
141        if (stmt != null && stmt.toString() != null) {
142            String text = stmt.toString().trim();
143            String[] parts = text.split("\\s+");
144            if (parts.length >= 2) {
145                currentDatabase = stripQuotes(parts[parts.length - 1]);
146            }
147        }
148    }
149
150    /**
151     * Process SET SCHEMA statement.
152     */
153    private void processSetSchema(TSetSchemaStmt setSchema) {
154        if (setSchema != null && setSchema.getSchemaName() != null) {
155            currentSchema = setSchema.getSchemaName().toString();
156        }
157    }
158
159    /**
160     * Strip quotes from an identifier.
161     */
162    private String stripQuotes(String name) {
163        if (name == null || name.isEmpty()) {
164            return name;
165        }
166        // Strip double quotes
167        if (name.startsWith("\"") && name.endsWith("\"") && name.length() > 2) {
168            return name.substring(1, name.length() - 1);
169        }
170        // Strip backticks
171        if (name.startsWith("`") && name.endsWith("`") && name.length() > 2) {
172            return name.substring(1, name.length() - 1);
173        }
174        // Strip brackets
175        if (name.startsWith("[") && name.endsWith("]") && name.length() > 2) {
176            return name.substring(1, name.length() - 1);
177        }
178        // Strip single quotes (for schema names in SET SCHEMA 'name')
179        if (name.startsWith("'") && name.endsWith("'") && name.length() > 2) {
180            return name.substring(1, name.length() - 1);
181        }
182        // Strip trailing semicolon
183        if (name.endsWith(";")) {
184            return name.substring(0, name.length() - 1);
185        }
186        return name;
187    }
188
189    /**
190     * Apply the tracked defaults to a TSQLEnv.
191     *
192     * @param env The SQL environment to update
193     */
194    public void applyDefaults(TSQLEnv env) {
195        if (env == null) {
196            return;
197        }
198
199        if (currentDatabase != null && !currentDatabase.isEmpty()) {
200            env.setDefaultCatalogName(currentDatabase);
201        }
202        if (currentSchema != null && !currentSchema.isEmpty()) {
203            env.setDefaultSchemaName(currentSchema);
204        }
205    }
206
207    /**
208     * Check if any context has been tracked.
209     *
210     * @return true if either database or schema context has been set
211     */
212    public boolean hasContext() {
213        return currentDatabase != null || currentSchema != null;
214    }
215
216    /**
217     * Get the current database name.
218     *
219     * @return The current database name, or null if not set
220     */
221    public String getCurrentDatabase() {
222        return currentDatabase;
223    }
224
225    /**
226     * Get the current schema name.
227     *
228     * @return The current schema name, or null if not set
229     */
230    public String getCurrentSchema() {
231        return currentSchema;
232    }
233
234    /**
235     * Reset the context tracker.
236     */
237    public void reset() {
238        currentDatabase = null;
239        currentSchema = null;
240    }
241
242    @Override
243    public String toString() {
244        return "DatabaseContextTracker{" +
245            "database='" + currentDatabase + '\'' +
246            ", schema='" + currentSchema + '\'' +
247            '}';
248    }
249}