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}