001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 004 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.List; 008import java.util.Objects; 009 010/** 011 * Represents a fully qualified name for a database object (table, view, etc.). 012 * 013 * <p>A qualified name can be represented in two ways: 014 * <ol> 015 * <li>As a list of parts (legacy mode) - for generic N-part names</li> 016 * <li>As explicit catalog/schema/name (structured mode) - for table names</li> 017 * </ol> 018 * 019 * <p>For table names, the structured fields provide: 020 * <ul> 021 * <li><b>catalog</b> (database) - The top-level container (e.g., "mydb")</li> 022 * <li><b>schema</b> - The schema within the catalog (e.g., "dbo", "public")</li> 023 * <li><b>name</b> - The object name itself (e.g., "users", "orders")</li> 024 * </ul> 025 * 026 * <h3>Usage Examples</h3> 027 * <pre>{@code 028 * // Legacy list-based construction 029 * QualifiedName q1 = new QualifiedName("schema", "table"); 030 * 031 * // Structured table name construction 032 * QualifiedName q2 = QualifiedName.forTable("mydb", "dbo", "users"); 033 * 034 * // Apply defaults for unqualified names 035 * QualifiedName q3 = QualifiedName.forTable(null, null, "users") 036 * .withDefaults("mydb", "dbo"); 037 * // Result: catalog=mydb, schema=dbo, name=users 038 * }</pre> 039 * 040 * @see QualifiedNameResolver 041 */ 042public class QualifiedName { 043 044 // ========== Legacy list-based storage ========== 045 private final List<String> parts; 046 047 // ========== Structured table name storage ========== 048 /** The catalog (database) part, or null if not specified */ 049 private final String catalog; 050 051 /** The schema part, or null if not specified */ 052 private final String schema; 053 054 /** The object name (table name), or null if using legacy mode */ 055 private final String tableName; 056 057 /** Whether this instance uses structured mode */ 058 private final boolean structuredMode; 059 060 // ========== Legacy Constructors ========== 061 062 /** 063 * Create a qualified name from a list of parts (legacy mode). 064 */ 065 public QualifiedName(List<String> parts) { 066 this.parts = new ArrayList<>(parts); 067 this.catalog = null; 068 this.schema = null; 069 this.tableName = null; 070 this.structuredMode = false; 071 } 072 073 /** 074 * Create a qualified name from varargs parts (legacy mode). 075 */ 076 public QualifiedName(String... parts) { 077 this.parts = Arrays.asList(parts); 078 this.catalog = null; 079 this.schema = null; 080 this.tableName = null; 081 this.structuredMode = false; 082 } 083 084 // ========== Structured Constructors ========== 085 086 /** 087 * Private constructor for structured mode. 088 */ 089 private QualifiedName(String catalog, String schema, String tableName, boolean structured) { 090 this.catalog = normalizeEmpty(catalog); 091 this.schema = normalizeEmpty(schema); 092 this.tableName = tableName; 093 this.structuredMode = structured; 094 095 // Also populate parts for backward compatibility 096 List<String> partsList = new ArrayList<>(); 097 if (this.catalog != null) partsList.add(this.catalog); 098 if (this.schema != null) partsList.add(this.schema); 099 if (this.tableName != null) partsList.add(this.tableName); 100 this.parts = partsList; 101 } 102 103 /** 104 * Create a structured qualified name for a table. 105 * 106 * @param catalog The catalog/database name (nullable) 107 * @param schema The schema name (nullable) 108 * @param tableName The table name (required) 109 * @return A new QualifiedName in structured mode 110 */ 111 public static QualifiedName forTable(String catalog, String schema, String tableName) { 112 if (tableName == null || tableName.isEmpty()) { 113 throw new IllegalArgumentException("Table name cannot be null or empty"); 114 } 115 return new QualifiedName(catalog, schema, tableName, true); 116 } 117 118 /** 119 * Create a qualified name from just the table name. 120 */ 121 public static QualifiedName forTable(String tableName) { 122 return forTable(null, null, tableName); 123 } 124 125 /** 126 * Create a qualified name from schema and table name. 127 */ 128 public static QualifiedName forTable(String schema, String tableName) { 129 return forTable(null, schema, tableName); 130 } 131 132 private static String normalizeEmpty(String value) { 133 return (value == null || value.isEmpty()) ? null : value; 134 } 135 136 // ========== Legacy Getters ========== 137 138 public List<String> getParts() { 139 return new ArrayList<>(parts); 140 } 141 142 public int getPartCount() { 143 return parts.size(); 144 } 145 146 public String getPart(int index) { 147 return parts.get(index); 148 } 149 150 public String getFirstPart() { 151 return parts.isEmpty() ? null : parts.get(0); 152 } 153 154 public String getLastPart() { 155 return parts.isEmpty() ? null : parts.get(parts.size() - 1); 156 } 157 158 // ========== Structured Getters ========== 159 160 /** 161 * Get the catalog (database) part. 162 * Only meaningful in structured mode. 163 */ 164 public String getCatalog() { 165 return catalog; 166 } 167 168 /** 169 * Get the schema part. 170 * Only meaningful in structured mode. 171 */ 172 public String getSchema() { 173 return schema; 174 } 175 176 /** 177 * Get the table name. 178 * In structured mode, returns the explicit table name. 179 * In legacy mode, returns the last part. 180 */ 181 public String getTableName() { 182 if (structuredMode) { 183 return tableName; 184 } 185 return getLastPart(); 186 } 187 188 /** 189 * Alias for getTableName() - returns the object name. 190 */ 191 public String getName() { 192 return getTableName(); 193 } 194 195 /** 196 * Check if this instance uses structured mode. 197 */ 198 public boolean isStructuredMode() { 199 return structuredMode; 200 } 201 202 /** 203 * Check if this qualified name has a catalog specified. 204 */ 205 public boolean hasCatalog() { 206 return catalog != null; 207 } 208 209 /** 210 * Check if this qualified name has a schema specified. 211 */ 212 public boolean hasSchema() { 213 return schema != null; 214 } 215 216 /** 217 * Check if this is a fully qualified name (all three parts in structured mode). 218 */ 219 public boolean isFullyQualified() { 220 return structuredMode && catalog != null && schema != null && tableName != null; 221 } 222 223 // ========== Legacy Operations ========== 224 225 /** 226 * Returns a new qualified name with the first part removed (legacy mode). 227 */ 228 public QualifiedName removeFirst() { 229 if (parts.size() <= 1) { 230 return new QualifiedName(); 231 } 232 return new QualifiedName(parts.subList(1, parts.size())); 233 } 234 235 /** 236 * Returns a new qualified name with an additional part prepended (legacy mode). 237 */ 238 public QualifiedName prepend(String part) { 239 List<String> newParts = new ArrayList<>(); 240 newParts.add(part); 241 newParts.addAll(parts); 242 return new QualifiedName(newParts); 243 } 244 245 /** 246 * Returns a new qualified name with an additional part appended (legacy mode). 247 */ 248 public QualifiedName append(String part) { 249 List<String> newParts = new ArrayList<>(parts); 250 newParts.add(part); 251 return new QualifiedName(newParts); 252 } 253 254 // ========== Structured Operations ========== 255 256 /** 257 * Create a new qualified name by filling in missing parts from defaults. 258 * 259 * <p>This does NOT override existing parts - it only fills in nulls. 260 * Only works in structured mode. 261 * 262 * @param defaultCatalog The default catalog to use if this.catalog is null 263 * @param defaultSchema The default schema to use if this.schema is null 264 * @return A new qualified name with defaults applied 265 */ 266 public QualifiedName withDefaults(String defaultCatalog, String defaultSchema) { 267 if (!structuredMode) { 268 // In legacy mode, convert to structured mode first 269 String name = getLastPart(); 270 String schemaFromParts = parts.size() >= 2 ? parts.get(parts.size() - 2) : null; 271 String catalogFromParts = parts.size() >= 3 ? parts.get(parts.size() - 3) : null; 272 return new QualifiedName( 273 catalogFromParts != null ? catalogFromParts : defaultCatalog, 274 schemaFromParts != null ? schemaFromParts : defaultSchema, 275 name, 276 true 277 ); 278 } 279 return new QualifiedName( 280 this.catalog != null ? this.catalog : defaultCatalog, 281 this.schema != null ? this.schema : defaultSchema, 282 this.tableName, 283 true 284 ); 285 } 286 287 /** 288 * Create a fully qualified name by applying defaults for all missing parts. 289 * 290 * @param defaultCatalog The default catalog 291 * @param defaultSchema The default schema 292 * @return A fully qualified name (all three parts set) 293 */ 294 public QualifiedName toFullyQualified(String defaultCatalog, String defaultSchema) { 295 return withDefaults(defaultCatalog, defaultSchema); 296 } 297 298 // ========== Matching ========== 299 300 /** 301 * Check if this qualified name matches another, using the given name matcher. 302 * 303 * <p>Matching rules: 304 * <ul> 305 * <li>Object names must always match</li> 306 * <li>If both have schemas, they must match</li> 307 * <li>If both have catalogs, they must match</li> 308 * <li>Missing parts (null) are treated as wildcards</li> 309 * </ul> 310 * 311 * @param other The other qualified name to compare 312 * @param matcher The name matcher for comparison (handles case sensitivity) 313 * @return true if the names match according to the rules 314 */ 315 public boolean matches(QualifiedName other, INameMatcher matcher) { 316 if (other == null) { 317 return false; 318 } 319 320 // Object names must always match 321 String thisName = this.getTableName(); 322 String otherName = other.getTableName(); 323 if (thisName == null || otherName == null) { 324 return false; 325 } 326 if (!matcher.matches(thisName, otherName)) { 327 return false; 328 } 329 330 // For structured mode, also check schema and catalog 331 if (this.structuredMode || other.structuredMode) { 332 String thisSchema = this.getSchema(); 333 String otherSchema = other.getSchema(); 334 335 // If both have schemas, they must match 336 if (thisSchema != null && otherSchema != null) { 337 if (!matcher.matches(thisSchema, otherSchema)) { 338 return false; 339 } 340 } 341 342 String thisCatalog = this.getCatalog(); 343 String otherCatalog = other.getCatalog(); 344 345 // If both have catalogs, they must match 346 if (thisCatalog != null && otherCatalog != null) { 347 if (!matcher.matches(thisCatalog, otherCatalog)) { 348 return false; 349 } 350 } 351 } 352 353 return true; 354 } 355 356 /** 357 * Check exact match (all parts must match, including nulls). 358 */ 359 public boolean matchesExact(QualifiedName other, INameMatcher matcher) { 360 if (other == null) return false; 361 362 // Check table name 363 String thisName = this.getTableName(); 364 String otherName = other.getTableName(); 365 if (!Objects.equals(thisName, otherName) && 366 (thisName == null || otherName == null || !matcher.matches(thisName, otherName))) { 367 return false; 368 } 369 370 // Check schema (must both be null or both match) 371 String thisSchema = this.getSchema(); 372 String otherSchema = other.getSchema(); 373 if (thisSchema == null != (otherSchema == null)) return false; 374 if (thisSchema != null && !matcher.matches(thisSchema, otherSchema)) return false; 375 376 // Check catalog (must both be null or both match) 377 String thisCatalog = this.getCatalog(); 378 String otherCatalog = other.getCatalog(); 379 if (thisCatalog == null != (otherCatalog == null)) return false; 380 if (thisCatalog != null && !matcher.matches(thisCatalog, otherCatalog)) return false; 381 382 return true; 383 } 384 385 // ========== Key Generation ========== 386 387 /** 388 * Create a key suitable for use in maps/sets. 389 * 390 * <p>The key is lowercase and includes all three parts separated by null characters 391 * to ensure uniqueness. 392 */ 393 public String toNormalizedKey() { 394 String c = catalog != null ? catalog.toLowerCase() : ""; 395 String s = schema != null ? schema.toLowerCase() : ""; 396 String n = getTableName() != null ? getTableName().toLowerCase() : ""; 397 return c + "\0" + s + "\0" + n; 398 } 399 400 // ========== String Conversion ========== 401 402 @Override 403 public String toString() { 404 if (structuredMode) { 405 StringBuilder sb = new StringBuilder(); 406 if (catalog != null) { 407 sb.append(catalog).append("."); 408 } 409 if (schema != null) { 410 sb.append(schema).append("."); 411 } 412 if (tableName != null) { 413 sb.append(tableName); 414 } 415 return sb.toString(); 416 } 417 return String.join(".", parts); 418 } 419 420 /** 421 * Convert to a fully qualified string representation, using placeholders for missing parts. 422 * 423 * @param catalogPlaceholder Placeholder for missing catalog (e.g., "*" or "?") 424 * @param schemaPlaceholder Placeholder for missing schema 425 * @return String like "*.*.name" or "catalog.*.name" 426 */ 427 public String toStringWithPlaceholders(String catalogPlaceholder, String schemaPlaceholder) { 428 String c = catalog != null ? catalog : catalogPlaceholder; 429 String s = schema != null ? schema : schemaPlaceholder; 430 String n = getTableName() != null ? getTableName() : "?"; 431 return c + "." + s + "." + n; 432 } 433 434 // ========== Equality ========== 435 436 @Override 437 public boolean equals(Object o) { 438 if (this == o) return true; 439 if (o == null || getClass() != o.getClass()) return false; 440 QualifiedName that = (QualifiedName) o; 441 if (this.structuredMode || that.structuredMode) { 442 return Objects.equals(catalog, that.catalog) && 443 Objects.equals(schema, that.schema) && 444 Objects.equals(getTableName(), that.getTableName()); 445 } 446 return Objects.equals(parts, that.parts); 447 } 448 449 @Override 450 public int hashCode() { 451 if (structuredMode) { 452 return Objects.hash(catalog, schema, tableName); 453 } 454 return Objects.hash(parts); 455 } 456 457 /** 458 * Check equality ignoring case. 459 * 460 * @param other The other qualified name 461 * @return true if all parts match (ignoring case) 462 */ 463 public boolean equalsIgnoreCase(QualifiedName other) { 464 if (other == null) return false; 465 466 String thisName = this.getTableName(); 467 String otherName = other.getTableName(); 468 if (thisName == null || otherName == null) { 469 if (thisName != otherName) return false; 470 } else if (!thisName.equalsIgnoreCase(otherName)) { 471 return false; 472 } 473 474 String thisSchema = this.getSchema(); 475 String otherSchema = other.getSchema(); 476 if (thisSchema == null != (otherSchema == null)) return false; 477 if (thisSchema != null && !thisSchema.equalsIgnoreCase(otherSchema)) return false; 478 479 String thisCatalog = this.getCatalog(); 480 String otherCatalog = other.getCatalog(); 481 if (thisCatalog == null != (otherCatalog == null)) return false; 482 if (thisCatalog != null && !thisCatalog.equalsIgnoreCase(otherCatalog)) return false; 483 484 return true; 485 } 486}