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}