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