001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.nodes.TObjectName; 005import gudusoft.gsqlparser.nodes.TTable; 006import gudusoft.gsqlparser.sqlenv.TSQLEnv; 007import gudusoft.gsqlparser.util.SQLUtil; 008 009/** 010 * Service for normalizing table names into fully qualified names. 011 * 012 * <p>This class handles the conversion of potentially partial table names 013 * (e.g., just "table", "schema.table", or "catalog.schema.table") into 014 * fully qualified {@link QualifiedName} objects by applying default 015 * catalog and schema from {@link TSQLEnv}. 016 * 017 * <h3>Normalization Rules</h3> 018 * <table border="1"> 019 * <tr><th>Input</th><th>Rule</th><th>Output</th></tr> 020 * <tr><td>table</td><td>Apply both defaults</td><td>(defaultCatalog, defaultSchema, table)</td></tr> 021 * <tr><td>schema.table</td><td>Apply catalog default only</td><td>(defaultCatalog, schema, table)</td></tr> 022 * <tr><td>catalog.schema.table</td><td>No defaults applied</td><td>(catalog, schema, table)</td></tr> 023 * </table> 024 * 025 * <h3>Usage Example</h3> 026 * <pre>{@code 027 * // Setup 028 * TSQLEnv env = new TSQLEnv(EDbVendor.dbvmssql); 029 * env.setDefaultCatalogName("mydb"); 030 * env.setDefaultSchemaName("dbo"); 031 * 032 * QualifiedNameResolver resolver = new QualifiedNameResolver(env, EDbVendor.dbvmssql); 033 * 034 * // Normalize a simple table name 035 * QualifiedName q1 = resolver.resolve(tableNode); 036 * // Result: (mydb, dbo, tableName) 037 * 038 * // Normalize a two-part name 039 * QualifiedName q2 = resolver.resolve("sales", "orders"); 040 * // Result: (mydb, sales, orders) 041 * 042 * // Three-part name is unchanged 043 * QualifiedName q3 = resolver.resolve("otherdb", "hr", "employees"); 044 * // Result: (otherdb, hr, employees) 045 * }</pre> 046 * 047 * @see QualifiedName 048 * @see TSQLEnv 049 */ 050public class QualifiedNameResolver { 051 052 /** The SQL environment containing default catalog/schema */ 053 private final TSQLEnv sqlEnv; 054 055 /** The database vendor (affects parsing behavior) */ 056 private final EDbVendor vendor; 057 058 /** Default catalog to use when not provided */ 059 private final String defaultCatalog; 060 061 /** Default schema to use when not provided */ 062 private final String defaultSchema; 063 064 /** 065 * Create a QualifiedNameResolver with the given SQL environment. 066 * 067 * @param sqlEnv The SQL environment containing default catalog/schema 068 * @param vendor The database vendor 069 */ 070 public QualifiedNameResolver(TSQLEnv sqlEnv, EDbVendor vendor) { 071 this.sqlEnv = sqlEnv; 072 this.vendor = vendor; 073 this.defaultCatalog = sqlEnv != null ? sqlEnv.getDefaultCatalogName() : null; 074 this.defaultSchema = sqlEnv != null ? sqlEnv.getDefaultSchemaName() : null; 075 } 076 077 /** 078 * Create a QualifiedNameResolver with explicit defaults. 079 * 080 * @param defaultCatalog The default catalog name 081 * @param defaultSchema The default schema name 082 * @param vendor The database vendor 083 */ 084 public QualifiedNameResolver(String defaultCatalog, String defaultSchema, EDbVendor vendor) { 085 this.sqlEnv = null; 086 this.vendor = vendor; 087 this.defaultCatalog = defaultCatalog; 088 this.defaultSchema = defaultSchema; 089 } 090 091 /** 092 * Get the current default catalog. 093 * 094 * <p>If sqlEnv was provided, returns the current value from sqlEnv 095 * (which may have changed since construction). 096 */ 097 public String getDefaultCatalog() { 098 if (sqlEnv != null) { 099 return sqlEnv.getDefaultCatalogName(); 100 } 101 return defaultCatalog; 102 } 103 104 /** 105 * Get the current default schema. 106 * 107 * <p>If sqlEnv was provided, returns the current value from sqlEnv 108 * (which may have changed since construction). 109 */ 110 public String getDefaultSchema() { 111 if (sqlEnv != null) { 112 return sqlEnv.getDefaultSchemaName(); 113 } 114 return defaultSchema; 115 } 116 117 /** 118 * Resolve a TTable's name to a fully qualified name. 119 * 120 * @param table The table node from the AST 121 * @return A normalized QualifiedName with defaults applied 122 */ 123 public QualifiedName resolve(TTable table) { 124 if (table == null) { 125 return null; 126 } 127 return resolve(table.getTableName()); 128 } 129 130 /** 131 * Resolve a TObjectName (table reference) to a fully qualified name. 132 * 133 * @param tableName The table name node from the AST 134 * @return A normalized QualifiedName with defaults applied 135 */ 136 public QualifiedName resolve(TObjectName tableName) { 137 if (tableName == null) { 138 return null; 139 } 140 141 // Extract parts from the TObjectName 142 String catalog = null; 143 String schema = null; 144 String name = null; 145 146 // TObjectName stores: database.schema.object or schema.object or object 147 // For table names: databaseToken -> catalogToken 148 // schemaToken -> schemaToken 149 // objectToken -> tableToken (but getTableString() returns object name) 150 151 // Get the object/table name (required) 152 name = tableName.getObjectString(); 153 if (name == null || name.isEmpty()) { 154 // Fallback to full string representation 155 name = tableName.toString(); 156 if (name == null || name.isEmpty()) { 157 return null; 158 } 159 // Parse the string manually 160 return parseQualifiedString(name); 161 } 162 163 // Get schema if present 164 if (tableName.getSchemaToken() != null) { 165 schema = tableName.getSchemaString(); 166 } 167 168 // Get database/catalog if present 169 if (tableName.getDatabaseToken() != null) { 170 catalog = tableName.getDatabaseString(); 171 } 172 173 // Create QualifiedName and apply defaults 174 return normalizeQualifiedName(catalog, schema, name); 175 } 176 177 /** 178 * Resolve a simple table name string to a fully qualified name. 179 * 180 * @param tableName The table name (may include dots) 181 * @return A normalized QualifiedName with defaults applied 182 */ 183 public QualifiedName resolve(String tableName) { 184 if (tableName == null || tableName.isEmpty()) { 185 return null; 186 } 187 return parseQualifiedString(tableName); 188 } 189 190 /** 191 * Create a qualified name from explicit parts with defaults applied. 192 * 193 * @param catalog The catalog (null to use default) 194 * @param schema The schema (null to use default) 195 * @param name The table name (required) 196 * @return A normalized QualifiedName 197 */ 198 public QualifiedName resolve(String catalog, String schema, String name) { 199 if (name == null || name.isEmpty()) { 200 return null; 201 } 202 return normalizeQualifiedName(catalog, schema, name); 203 } 204 205 /** 206 * Parse a dot-separated qualified name string. 207 * 208 * @param qualifiedString A string like "table", "schema.table", or "catalog.schema.table" 209 * @return A normalized QualifiedName with defaults applied 210 */ 211 private QualifiedName parseQualifiedString(String qualifiedString) { 212 if (qualifiedString == null || qualifiedString.isEmpty()) { 213 return null; 214 } 215 216 // Preserve quoted identifiers and intentionally omitted schema components. 217 String[] parts = splitQualifiedName(qualifiedString); 218 219 String catalog = null; 220 String schema = null; 221 String name; 222 223 if (parts.length == 1) { 224 // Just table name 225 name = parts[0]; 226 } else if (parts.length == 2) { 227 // schema.table 228 schema = parts[0]; 229 name = parts[1]; 230 } else if (parts.length >= 3) { 231 // catalog.schema.table (use last three parts) 232 int offset = parts.length - 3; 233 catalog = parts[offset]; 234 schema = parts[offset + 1]; 235 name = parts[offset + 2]; 236 } else { 237 return null; 238 } 239 240 return normalizeQualifiedName(catalog, schema, name); 241 } 242 243 /** 244 * Split a qualified name string into parts, handling quoted identifiers. 245 */ 246 private String[] splitQualifiedName(String qualifiedString) { 247 // The shared splitter preserves empty schema slots and quoted dots. 248 return SQLUtil.parseNames(qualifiedString, vendor).toArray(new String[0]); 249 } 250 251 /** 252 * Apply normalization rules to create a fully qualified name. 253 * 254 * <p>Rules: 255 * <ul> 256 * <li>If catalog is provided, use it (don't override with default)</li> 257 * <li>If schema is provided, use it (don't override with default)</li> 258 * <li>If catalog is null and we have a default, apply it</li> 259 * <li>If schema is null and we have a default, apply it</li> 260 * </ul> 261 * 262 * <p>Special case for 2-part names: 263 * <ul> 264 * <li>schema.table -> (defaultCatalog, schema, table) - schema is kept, catalog applied</li> 265 * </ul> 266 */ 267 private QualifiedName normalizeQualifiedName(String catalog, String schema, String name) { 268 if (name == null || name.isEmpty()) { 269 return null; 270 } 271 272 // Normalize empty strings to null 273 catalog = normalizeEmpty(catalog); 274 schema = normalizeEmpty(schema); 275 276 // Snowflake's database..object form always selects PUBLIC, regardless 277 // of the session schema. Unqualified names still use session defaults. 278 if (vendor == EDbVendor.dbvsnowflake && catalog != null && schema == null) { 279 schema = "PUBLIC"; 280 } 281 282 // Get current defaults from sqlEnv (may have changed) 283 String defCatalog = getDefaultCatalog(); 284 String defSchema = getDefaultSchema(); 285 286 // Apply defaults only for missing parts 287 // Key: Don't override explicitly provided parts 288 String finalCatalog = catalog != null ? catalog : defCatalog; 289 String finalSchema = schema != null ? schema : defSchema; 290 291 return QualifiedName.forTable(finalCatalog, finalSchema, name); 292 } 293 294 private String normalizeEmpty(String value) { 295 return (value == null || value.isEmpty()) ? null : value; 296 } 297 298 /** 299 * Check if two qualified names refer to the same table. 300 * 301 * <p>Both names are first normalized using current defaults, 302 * then compared for equality. 303 * 304 * @param name1 First qualified name (may be partial) 305 * @param name2 Second qualified name (may be partial) 306 * @return true if they refer to the same fully-qualified table 307 */ 308 public boolean isSameTable(QualifiedName name1, QualifiedName name2) { 309 if (name1 == null || name2 == null) { 310 return false; 311 } 312 313 // Apply defaults to both 314 QualifiedName normalized1 = name1.withDefaults(getDefaultCatalog(), getDefaultSchema()); 315 QualifiedName normalized2 = name2.withDefaults(getDefaultCatalog(), getDefaultSchema()); 316 317 return normalized1.equalsIgnoreCase(normalized2); 318 } 319 320 /** 321 * Check if a qualified name matches a target name, considering defaults. 322 * 323 * @param reference The reference being checked (may be partial) 324 * @param target The target table name (should be fully qualified) 325 * @return true if the reference resolves to the target 326 */ 327 public boolean matches(QualifiedName reference, QualifiedName target) { 328 if (reference == null || target == null) { 329 return false; 330 } 331 332 // Normalize the reference using defaults 333 QualifiedName normalizedRef = reference.withDefaults(getDefaultCatalog(), getDefaultSchema()); 334 335 // Compare with target 336 return normalizedRef.equalsIgnoreCase(target); 337 } 338 339 @Override 340 public String toString() { 341 return String.format("QualifiedNameResolver{defaultCatalog='%s', defaultSchema='%s', vendor=%s}", 342 getDefaultCatalog(), getDefaultSchema(), vendor); 343 } 344}