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}