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}