001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.nodes.TTable;
005import gudusoft.gsqlparser.nodes.TColumnDefinition;
006import gudusoft.gsqlparser.nodes.TColumnDefinitionList;
007import gudusoft.gsqlparser.nodes.TObjectName;
008import gudusoft.gsqlparser.resolver2.ColumnLevel;
009import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
010import gudusoft.gsqlparser.resolver2.model.ColumnSource;
011import gudusoft.gsqlparser.resolver2.model.QualifiedName;
012import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver;
013import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
014import gudusoft.gsqlparser.sqlenv.TSQLEnv;
015import gudusoft.gsqlparser.sqlenv.TSQLTable;
016import gudusoft.gsqlparser.sqlenv.TSQLColumn;
017
018import java.util.LinkedHashMap;
019import java.util.Map;
020
021/**
022 * Namespace representing a physical table.
023 * Provides column information from table metadata.
024 *
025 * Column sources come from:
026 * 1. Table metadata (if available from DDL)
027 * 2. External metadata providers (SQLEnv)
028 * 3. Inferred from usage (when no metadata available)
029 */
030public class TableNamespace extends AbstractNamespace {
031
032    private final TTable table;
033
034    /** TSQLEnv for looking up table metadata */
035    private TSQLEnv sqlEnv;
036
037    /** Database vendor for qualified name resolution */
038    private EDbVendor vendor;
039
040    /** Qualified name resolver for normalizing table references */
041    private QualifiedNameResolver qualifiedNameResolver;
042
043    /** The qualified name of this table (with defaults applied) */
044    private QualifiedName qualifiedName;
045
046    /** Flag to track if this table has actual metadata (vs. only inferred columns) */
047    private boolean hasMetadata = false;
048
049    /** The resolved TSQLTable from SQLEnv (if found) */
050    private TSQLTable resolvedTable;
051
052    public TableNamespace(TTable table, INameMatcher nameMatcher) {
053        super(table, nameMatcher);
054        this.table = table;
055    }
056
057    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv) {
058        super(table, nameMatcher);
059        this.table = table;
060        this.sqlEnv = sqlEnv;
061        // Default vendor - use setVendor() to override
062        if (sqlEnv != null) {
063            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, EDbVendor.dbvoracle);
064        }
065    }
066
067    /**
068     * Create a TableNamespace with full qualified name resolution support.
069     *
070     * @param table The table AST node
071     * @param nameMatcher The name matcher for case sensitivity
072     * @param sqlEnv The SQL environment for metadata lookup
073     * @param vendor The database vendor
074     */
075    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv, EDbVendor vendor) {
076        super(table, nameMatcher);
077        this.table = table;
078        this.sqlEnv = sqlEnv;
079        this.vendor = vendor;
080        if (sqlEnv != null) {
081            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
082        }
083    }
084
085    public TableNamespace(TTable table) {
086        super(table);
087        this.table = table;
088    }
089
090    /**
091     * Set the TSQLEnv for metadata lookup.
092     * @param sqlEnv the SQL environment containing table metadata
093     */
094    public void setSqlEnv(TSQLEnv sqlEnv) {
095        this.sqlEnv = sqlEnv;
096        if (sqlEnv != null && vendor != null) {
097            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
098        }
099        // Reset cached qualified name so it's recomputed with new env
100        this.qualifiedName = null;
101    }
102
103    /**
104     * Get the TSQLEnv used for metadata lookup.
105     * @return the SQL environment, or null if not set
106     */
107    public TSQLEnv getSqlEnv() {
108        return sqlEnv;
109    }
110
111    /**
112     * Set the database vendor.
113     * @param vendor the database vendor
114     */
115    public void setVendor(EDbVendor vendor) {
116        this.vendor = vendor;
117        if (sqlEnv != null) {
118            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
119        }
120        // Reset cached qualified name
121        this.qualifiedName = null;
122    }
123
124    /**
125     * Get the database vendor.
126     * @return the database vendor, or null if not set
127     */
128    public EDbVendor getVendor() {
129        return vendor;
130    }
131
132    /**
133     * Get the qualified name resolver.
134     * @return the resolver, or null if sqlEnv is not set
135     */
136    public QualifiedNameResolver getQualifiedNameResolver() {
137        return qualifiedNameResolver;
138    }
139
140    /**
141     * Get the fully qualified name of this table.
142     *
143     * <p>The qualified name is computed by applying defaults from TSQLEnv
144     * to the table's partial name.
145     *
146     * @return the qualified name, or null if table name is unavailable
147     */
148    public QualifiedName getQualifiedName() {
149        if (qualifiedName != null) {
150            return qualifiedName;
151        }
152
153        TObjectName tableName = table.getTableName();
154        if (tableName == null) {
155            return null;
156        }
157
158        if (qualifiedNameResolver != null) {
159            qualifiedName = qualifiedNameResolver.resolve(tableName);
160        } else {
161            // Fallback: build from table name parts without defaults
162            String catalog = tableName.getDatabaseString();
163            String schema = tableName.getSchemaString();
164            String name = tableName.getObjectString();
165            if (name == null || name.isEmpty()) {
166                name = tableName.toString();
167            }
168            if (name != null && !name.isEmpty()) {
169                qualifiedName = QualifiedName.forTable(catalog, schema, name);
170            }
171        }
172
173        return qualifiedName;
174    }
175
176    /**
177     * Get the resolved TSQLTable from SQLEnv.
178     * @return the resolved table, or null if not found in SQLEnv
179     */
180    public TSQLTable getResolvedTable() {
181        return resolvedTable;
182    }
183
184    @Override
185    public String getDisplayName() {
186        String alias = table.getAliasName();
187        if (alias != null && !alias.isEmpty()) {
188            return alias;
189        }
190        String fullName = table.getFullName();
191        if (fullName != null && !fullName.isEmpty()) {
192            return fullName;
193        }
194        // Fallback to simple table name
195        String name = table.getName();
196        if (name != null && !name.isEmpty()) {
197            return name;
198        }
199        // Last resort: try table name object
200        TObjectName tableName = table.getTableName();
201        if (tableName != null) {
202            String tableStr = tableName.getTableString();
203            if (tableStr != null && !tableStr.isEmpty()) {
204                return tableStr;
205            }
206            String tableNameStr = tableName.toString();
207            if (tableNameStr != null && !tableNameStr.isEmpty()) {
208                return tableNameStr;
209            }
210        }
211        return "";
212    }
213
214    @Override
215    public TTable getFinalTable() {
216        return table;
217    }
218
219    @Override
220    public TTable getSourceTable() {
221        return table;
222    }
223
224    @Override
225    protected void doValidate() {
226        // Load columns from metadata
227        columnSources = new LinkedHashMap<>();
228        hasMetadata = false;
229        resolvedTable = null;
230
231        // Priority 1: Check if table has column list (from DDL like CREATE TABLE)
232        TColumnDefinitionList columnDefs = table.getColumnDefinitions();
233        if (columnDefs != null && columnDefs.size() > 0) {
234            // Has explicit column list from CREATE TABLE or metadata
235            hasMetadata = true;
236            for (int i = 0; i < columnDefs.size(); i++) {
237                TColumnDefinition colDef = columnDefs.getColumn(i);
238                TObjectName colNameObj = colDef.getColumnName();
239                if (colNameObj == null) continue;
240
241                String colName = colNameObj.toString();
242
243                ColumnSource source = new ColumnSource(
244                    this,
245                    colName,
246                    colDef,
247                    1.0,  // Definite - from DDL metadata
248                    "ddl_metadata"
249                );
250                columnSources.put(colName, source);
251            }
252            return; // DDL metadata takes priority
253        }
254
255        // Priority 2: Try to load from TSQLEnv if available
256        if (sqlEnv != null) {
257            TObjectName tableName = table.getTableName();
258            TSQLTable sqlTable = null;
259            if (tableName != null) {
260                sqlTable = sqlEnv.searchTable(tableName);
261            }
262
263            if (sqlTable != null) {
264                // Found table in SQLEnv - load all columns
265                resolvedTable = sqlTable;
266                hasMetadata = true;
267
268                for (TSQLColumn column : sqlTable.getColumnList()) {
269                    String colName = column.getName();
270
271                    ColumnSource source = new ColumnSource(
272                        this,
273                        colName,
274                        null,  // No TColumnDefinition node from SQLEnv
275                        1.0,   // Definite - from SQLEnv metadata
276                        "sqlenv_metadata"
277                    );
278                    columnSources.put(colName, source);
279                }
280                return; // SQLEnv metadata found
281            }
282        }
283
284        // Priority 3: Check for known table-valued functions with well-defined output columns
285        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) {
286            String funcName = getTableFunctionName();
287            if (funcName != null) {
288                String upperName = funcName.toUpperCase();
289                if (upperName.equals("STRING_SPLIT")) {
290                    // STRING_SPLIT returns: value, ordinal (ordinal is SQL Server 2022+)
291                    hasMetadata = true;
292                    addKnownFunctionColumn("value");
293                    addKnownFunctionColumn("ordinal");
294                    return;
295                }
296                // Add more known table functions here as needed
297                // e.g., OPENJSON, OPENXML, etc.
298            }
299        }
300
301        // Priority 4: No metadata available
302        // For tables without metadata, we'll handle this as MAYBE in hasColumn
303        // Columns will be inferred from usage in resolveColumn()
304    }
305
306    /**
307     * Get the function name for table-valued functions.
308     */
309    private String getTableFunctionName() {
310        if (table.getTableName() != null) {
311            return table.getTableName().toString();
312        }
313        if (table.getFuncCall() != null && table.getFuncCall().getFunctionName() != null) {
314            return table.getFuncCall().getFunctionName().toString();
315        }
316        return table.getName();
317    }
318
319    /**
320     * Add a known column for a table-valued function.
321     */
322    private void addKnownFunctionColumn(String columnName) {
323        ColumnSource source = new ColumnSource(
324            this,
325            columnName,
326            null,
327            1.0,  // Definite - known function output column
328            "known_function_column"
329        );
330        columnSources.put(columnName, source);
331    }
332
333    @Override
334    public ColumnLevel hasColumn(String columnName) {
335        ensureValidated();
336
337        // Check if column exists in known columns (including inferred ones)
338        for (String existingCol : columnSources.keySet()) {
339            if (nameMatcher.matches(existingCol, columnName)) {
340                return ColumnLevel.EXISTS;
341            }
342        }
343
344        // If we don't have metadata, any column MAYBE exists
345        // (we can't definitively say it doesn't exist)
346        if (!hasMetadata) {
347            return ColumnLevel.MAYBE;
348        }
349
350        return ColumnLevel.NOT_EXISTS;
351    }
352
353    @Override
354    public ColumnSource resolveColumn(String columnName) {
355        ensureValidated();
356
357        // First try to find in known columns
358        ColumnSource existing = super.resolveColumn(columnName);
359        if (existing != null) {
360            return existing;
361        }
362
363        // If no metadata available, create an inferred ColumnSource
364        // This allows columns to be resolved against tables without metadata
365        // Note: hasMetadata is false when we don't have DDL or SQLEnv metadata
366        if (!hasMetadata && columnName != null && !columnName.isEmpty()) {
367            // Create a column source with moderate confidence
368            // Since we don't have metadata, we're inferring this column exists
369            ColumnSource inferred = new ColumnSource(
370                this,
371                columnName,
372                null,  // No definition node
373                0.8,   // Moderate confidence - we don't have proof this column exists
374                "inferred_from_usage"
375            );
376
377            // Cache it for future lookups
378            columnSources.put(columnName, inferred);
379
380            return inferred;
381        }
382
383        return null;
384    }
385
386    public TTable getTable() {
387        return table;
388    }
389
390    /**
391     * Returns true if this namespace has actual metadata (from DDL or SQLEnv).
392     * When false, column resolution will infer columns from usage.
393     *
394     * @return true if metadata is available, false if columns are inferred
395     */
396    public boolean hasMetadata() {
397        ensureValidated();
398        return hasMetadata;
399    }
400
401    /**
402     * Slice S4: authoritative metadata-state tri-value (plan §5.5).
403     *
404     * <p>Distinguishes:</p>
405     * <ul>
406     *   <li>{@link MetadataState#FOUND} — DDL columns present, or
407     *       {@link TSQLEnv} returned a table with a non-empty column list, or
408     *       a known table-valued function (e.g. STRING_SPLIT) populated its
409     *       columns.</li>
410     *   <li>{@link MetadataState#NOT_FOUND_IN_CATALOG} — a non-empty
411     *       {@code TSQLEnv} was consulted but the table itself is absent.
412     *       Never used when no environment has been configured.</li>
413     *   <li>{@link MetadataState#METADATA_UNAVAILABLE} — no environment, an
414     *       empty environment (no catalogs registered), the lookup returned a
415     *       table without expanded columns (Q8 view-without-columns), or any
416     *       other unauthoritative state.</li>
417     * </ul>
418     */
419    @Override
420    public MetadataState getMetadataState() {
421        ensureValidated();
422
423        // Authoritative: DDL columns or TSQLEnv-resolved table with columns.
424        if (hasMetadata && columnSources != null && !columnSources.isEmpty()) {
425            return MetadataState.FOUND;
426        }
427
428        // Found-but-unexpanded: TSQLEnv returned the table but no columns came
429        // through. Q8 view-without-columns falls here. Never NOT_FOUND_IN_CATALOG
430        // and never UNKNOWN_COLUMN — strict mode may emit a WARNING in S6.
431        if (resolvedTable != null) {
432            return MetadataState.METADATA_UNAVAILABLE;
433        }
434
435        // Lookup ran against a populated environment and missed entirely. The
436        // env has at least one catalog registered, so we trust the absence as
437        // an authoritative table-not-found signal. Bridges pre-register an
438        // empty default catalog/schema during construction (see
439        // CatalogRuntimeToSQLEnvBridge.applyDefaults), which preserves this
440        // behavior under lazy materialization.
441        if (sqlEnv != null && hasAnyCatalogContent(sqlEnv)) {
442            return MetadataState.NOT_FOUND_IN_CATALOG;
443        }
444
445        return MetadataState.METADATA_UNAVAILABLE;
446    }
447
448    private static boolean hasAnyCatalogContent(TSQLEnv env) {
449        if (env == null) {
450            return false;
451        }
452        java.util.List<TSQLCatalog> catalogs = env.getCatalogList();
453        return catalogs != null && !catalogs.isEmpty();
454    }
455
456    /**
457     * Add a USING clause column to this table's namespace.
458     * This is called during scope building when a JOIN...USING clause
459     * includes columns from this table. The USING column exists in BOTH
460     * tables of the join.
461     *
462     * @param columnName the name of the USING column
463     */
464    public void addUsingColumn(String columnName) {
465        ensureValidated();
466
467        // Only add if not already present
468        if (hasColumn(columnName) == ColumnLevel.EXISTS) {
469            return;
470        }
471
472        ColumnSource source = new ColumnSource(
473            this,
474            columnName,
475            null,  // No definition node
476            1.0,   // Definite - USING clause semantics guarantee the column exists
477            "using_clause_column"
478        );
479        columnSources.put(columnName, source);
480    }
481
482    @Override
483    public String toString() {
484        return "TableNamespace(" + getDisplayName() + ", columns=" +
485               (columnSources != null ? columnSources.size() : "?") + ")";
486    }
487}