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