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}