001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.nodes.TTable;
006import gudusoft.gsqlparser.nodes.TColumnDefinition;
007import gudusoft.gsqlparser.nodes.TColumnDefinitionList;
008import gudusoft.gsqlparser.nodes.TExpression;
009import gudusoft.gsqlparser.nodes.TObjectName;
010import gudusoft.gsqlparser.nodes.TObjectNameList;
011import gudusoft.gsqlparser.resolver2.ColumnLevel;
012import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
013import gudusoft.gsqlparser.resolver2.model.ColumnSource;
014import gudusoft.gsqlparser.resolver2.model.QualifiedName;
015import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver;
016import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
017import gudusoft.gsqlparser.sqlenv.TSQLEnv;
018import gudusoft.gsqlparser.sqlenv.TSQLTable;
019import gudusoft.gsqlparser.sqlenv.TSQLColumn;
020
021import java.util.LinkedHashMap;
022import java.util.Map;
023
024/**
025 * Namespace representing a physical table.
026 * Provides column information from table metadata.
027 *
028 * Column sources come from:
029 * 1. Table metadata (if available from DDL)
030 * 2. External metadata providers (SQLEnv)
031 * 3. Inferred from usage (when no metadata available)
032 */
033public class TableNamespace extends AbstractNamespace {
034
035    private final TTable table;
036
037    /** TSQLEnv for looking up table metadata */
038    private TSQLEnv sqlEnv;
039
040    /** Database vendor for qualified name resolution */
041    private EDbVendor vendor;
042
043    /** Qualified name resolver for normalizing table references */
044    private QualifiedNameResolver qualifiedNameResolver;
045
046    /** The qualified name of this table (with defaults applied) */
047    private QualifiedName qualifiedName;
048
049    /** Flag to track if this table has actual metadata (vs. only inferred columns) */
050    private boolean hasMetadata = false;
051
052    /**
053     * Whether two generated outputs of a known table function ended up under ONE name — a partial
054     * alias list renames the leading outputs, and a renamed one can collide with a built-in name
055     * that follows ({@code STRING_SPLIT(...) AS f(ordinal)} renames {@code value} to {@code ordinal}
056     * and then meets the real {@code ordinal}). The name-keyed map then holds one of the two, so
057     * the enumerated set is NOT the whole schema and this namespace must not claim it is.
058     */
059    private boolean generatedNameCollision = false;
060
061    /** The resolved TSQLTable from SQLEnv (if found) */
062    private TSQLTable resolvedTable;
063    private java.util.Map<gudusoft.gsqlparser.sqlenv.CanonKey, String> catalogColumnKeys;
064
065    /** A namespace exposes catalog names bare; references still use SQL spelling. */
066    public String getColumnLookupSpelling(String exposedName) {
067        ensureValidated();
068        if (resolvedTable == null) return exposedName;
069        return gudusoft.gsqlparser.sqlenv.IdentifierCodec.encodeStored(
070                sqlEnv.getIdentifierService().getProfile(), ESQLDataObjectType.dotColumn, exposedName);
071    }
072
073    @Override
074    protected String findColumnKeyByMatcher(String lexicalName) {
075        if (catalogColumnKeys == null) return super.findColumnKeyByMatcher(lexicalName);
076        return catalogColumnKeys.get(sqlEnv.getIdentifierService().canonKeyOf(lexicalName,
077                ESQLDataObjectType.dotColumn, gudusoft.gsqlparser.sqlenv.IdentifierInputForm.SQL_LEXICAL));
078    }
079
080    private boolean hasStatementMetadata;
081    private TSQLTable statementMetadata;
082
083    public void setStatementMetadata(TSQLTable metadata, QualifiedNameResolver resolver) {
084        hasStatementMetadata = true;
085        statementMetadata = metadata;
086        qualifiedNameResolver = resolver;
087        qualifiedName = null;
088    }
089
090
091    public TableNamespace(TTable table, INameMatcher nameMatcher) {
092        super(table, nameMatcher);
093        this.table = table;
094    }
095
096    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv) {
097        super(table, nameMatcher);
098        this.table = table;
099        this.sqlEnv = sqlEnv;
100        // Default vendor - use setVendor() to override
101        if (sqlEnv != null) {
102            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, EDbVendor.dbvoracle);
103        }
104    }
105
106    /**
107     * Create a TableNamespace with full qualified name resolution support.
108     *
109     * @param table The table AST node
110     * @param nameMatcher The name matcher for case sensitivity
111     * @param sqlEnv The SQL environment for metadata lookup
112     * @param vendor The database vendor
113     */
114    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv, EDbVendor vendor) {
115        super(table, nameMatcher);
116        this.table = table;
117        this.sqlEnv = sqlEnv;
118        this.vendor = vendor;
119        if (sqlEnv != null) {
120            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
121        }
122    }
123
124    public TableNamespace(TTable table) {
125        super(table);
126        this.table = table;
127    }
128
129    /**
130     * Set the TSQLEnv for metadata lookup.
131     * @param sqlEnv the SQL environment containing table metadata
132     */
133    public void setSqlEnv(TSQLEnv sqlEnv) {
134        this.sqlEnv = sqlEnv;
135        if (sqlEnv != null && vendor != null) {
136            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
137        }
138        // Reset cached qualified name so it's recomputed with new env
139        this.qualifiedName = null;
140    }
141
142    /**
143     * Get the TSQLEnv used for metadata lookup.
144     * @return the SQL environment, or null if not set
145     */
146    public TSQLEnv getSqlEnv() {
147        return sqlEnv;
148    }
149
150    /**
151     * Set the database vendor.
152     * @param vendor the database vendor
153     */
154    public void setVendor(EDbVendor vendor) {
155        this.vendor = vendor;
156        if (sqlEnv != null) {
157            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
158        }
159        // Reset cached qualified name
160        this.qualifiedName = null;
161    }
162
163    /**
164     * Get the database vendor.
165     * @return the database vendor, or null if not set
166     */
167    public EDbVendor getVendor() {
168        return vendor;
169    }
170
171    /**
172     * Get the qualified name resolver.
173     * @return the resolver, or null if sqlEnv is not set
174     */
175    public QualifiedNameResolver getQualifiedNameResolver() {
176        return qualifiedNameResolver;
177    }
178
179    /**
180     * Get the fully qualified name of this table.
181     *
182     * <p>The qualified name is computed by applying defaults from TSQLEnv
183     * to the table's partial name.
184     *
185     * @return the qualified name, or null if table name is unavailable
186     */
187    public QualifiedName getQualifiedName() {
188        if (qualifiedName != null) {
189            return qualifiedName;
190        }
191
192        TObjectName tableName = table.getTableName();
193        if (tableName == null) {
194            return null;
195        }
196
197        if (qualifiedNameResolver != null) {
198            qualifiedName = qualifiedNameResolver.resolve(tableName);
199        } else {
200            // Fallback: build from table name parts without defaults
201            String catalog = tableName.getDatabaseString();
202            String schema = tableName.getSchemaString();
203            String name = tableName.getObjectString();
204            if (name == null || name.isEmpty()) {
205                name = tableName.toString();
206            }
207            if (name != null && !name.isEmpty()) {
208                qualifiedName = QualifiedName.forTable(catalog, schema, name);
209            }
210        }
211
212        return qualifiedName;
213    }
214
215    /**
216     * Get the resolved TSQLTable from SQLEnv.
217     * @return the resolved table, or null if not found in SQLEnv
218     */
219    public TSQLTable getResolvedTable() {
220        return resolvedTable;
221    }
222
223    @Override
224    public String getDisplayName() {
225        String alias = table.getAliasName();
226        if (alias != null && !alias.isEmpty()) {
227            return alias;
228        }
229        String fullName = table.getFullName();
230        if (fullName != null && !fullName.isEmpty()) {
231            return fullName;
232        }
233        // Fallback to simple table name
234        String name = table.getName();
235        if (name != null && !name.isEmpty()) {
236            return name;
237        }
238        // Last resort: try table name object
239        TObjectName tableName = table.getTableName();
240        if (tableName != null) {
241            String tableStr = tableName.getTableString();
242            if (tableStr != null && !tableStr.isEmpty()) {
243                return tableStr;
244            }
245            String tableNameStr = tableName.toString();
246            if (tableNameStr != null && !tableNameStr.isEmpty()) {
247                return tableNameStr;
248            }
249        }
250        return "";
251    }
252
253    @Override
254    public TTable getFinalTable() {
255        return table;
256    }
257
258    @Override
259    public TTable getSourceTable() {
260        return table;
261    }
262
263    @Override
264    protected void doValidate() {
265        // Load columns from metadata
266        columnSources = new LinkedHashMap<>();
267        hasMetadata = false;
268        generatedNameCollision = false;
269        resolvedTable = null;
270        catalogColumnKeys = null;
271
272        // Priority 1: Check if table has column list (from DDL like CREATE TABLE)
273        TColumnDefinitionList columnDefs = table.getColumnDefinitions();
274        if (columnDefs != null && columnDefs.size() > 0) {
275            // Has explicit column list from CREATE TABLE or metadata
276            hasMetadata = true;
277            for (int i = 0; i < columnDefs.size(); i++) {
278                TColumnDefinition colDef = columnDefs.getColumn(i);
279                TObjectName colNameObj = colDef.getColumnName();
280                if (colNameObj == null) continue;
281
282                String colName = colNameObj.toString();
283
284                ColumnSource source = new ColumnSource(
285                    this,
286                    colName,
287                    colDef,
288                    1.0,  // Definite - from DDL metadata
289                    "ddl_metadata"
290                );
291                columnSources.put(colName, source);
292            }
293            return; // DDL metadata takes priority
294        }
295
296        // Priority 2: Try to load from TSQLEnv if available
297        if (sqlEnv != null) {
298            TObjectName tableName = table.getTableName();
299            TSQLTable sqlTable = null;
300            if (tableName != null) {
301                sqlTable = hasStatementMetadata ? statementMetadata : sqlEnv.searchTable(tableName);
302            }
303
304            if (sqlTable != null) {
305                // Found table in SQLEnv - load all columns
306                resolvedTable = sqlTable;
307                catalogColumnKeys = new java.util.HashMap<>();
308                hasMetadata = true;
309
310                for (TSQLColumn column : sqlTable.getColumnList()) {
311                    String colName = column.getName();
312
313                    ColumnSource source = new ColumnSource(
314                        this,
315                        colName,
316                        null,  // No TColumnDefinition node from SQLEnv
317                        1.0,   // Definite - from SQLEnv metadata
318                        "sqlenv_metadata"
319                    );
320                    columnSources.put(colName, source);
321                    catalogColumnKeys.put(sqlEnv.getIdentifierService().canonKeyOf(colName,
322                            ESQLDataObjectType.dotColumn, gudusoft.gsqlparser.sqlenv.IdentifierInputForm.CATALOG_STORED), colName);
323                }
324                return; // SQLEnv metadata found
325            }
326        }
327
328        // Priority 3: Table-valued functions.
329        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) {
330            // 3a: a built-in whose columns the vendor fixes (TableFunctionLayout, the one rule the SQL-LR
331            //     producer publishes too): its names after an alias column list renames the leading ones
332            //     POSITIONALLY (FROM TABLE(FLATTEN(input => t.c)) f(s, k)), or after PostgreSQL's table alias
333            //     names the one column of a function of a base type (FROM unnest(a) AS u has the column u)
334            // a user table function the catalog declares (a CREATE FUNCTION of the input before the call): its
335            // declared columns, recorded on the call for every consumer of the layout
336            TableFunctionLayout.recordDeclared(table, sqlEnv);
337            TableFunctionLayout layout = TableFunctionLayout.of(table);
338            if (layout != null && layout.getNames() != null) {
339                for (String name : layout.getNames()) {
340                    if (name != null) {
341                        addKnownFunctionColumn(name);
342                    }
343                }
344                // an unnest of an array of unknown element type may have a composite's columns as well
345                hasMetadata = layout.isClosed();
346                return;
347            }
348            TObjectNameList aliasColumns = functionAliasColumns();
349
350            // 3b: an explicit column alias list names the leading output columns of a function
351            //     whose own columns are unknown
352            if (aliasColumns != null) {
353                for (int i = 0; i < aliasColumns.size(); i++) {
354                    TObjectName colName = aliasColumns.getObjectName(i);
355                    if (colName != null) {
356                        addKnownFunctionColumn(colName.toString());
357                    }
358                }
359                // The function's own output schema is unknown, so a partial list
360                // leaves unnamed columns that keep names we cannot enumerate.
361                // Stay non-authoritative: those must remain inferable rather than
362                // being reported as non-existent.
363                hasMetadata = false;
364                return;
365            }
366        }
367
368        // Priority 4: No metadata available
369        // For tables without metadata, we'll handle this as MAYBE in hasColumn
370        // Columns will be inferred from usage in resolveColumn()
371    }
372
373    /**
374     * The explicit column alias list of this table, or null when there is none.
375     *
376     * <p>Snowflake, PostgreSQL and Presto/Trino all allow renaming a table
377     * function's output columns: {@code FROM f(...) alias (c1, c2, ...)}. The list
378     * may be shorter than the function's output — the remaining columns then keep
379     * their original names.</p>
380     */
381    private TObjectNameList functionAliasColumns() {
382        if (table.getAliasClause() == null) {
383            return null;
384        }
385        TObjectNameList columns = table.getAliasClause().getColumns();
386        if (columns == null || columns.size() == 0) {
387            return null;
388        }
389        return columns;
390    }
391
392    /**
393     * Add a known column for a table-valued function.
394     */
395    private void addKnownFunctionColumn(String columnName) {
396        if (columnSources.containsKey(columnName)) {
397            generatedNameCollision = true;
398        }
399        ColumnSource source = new ColumnSource(
400            this,
401            columnName,
402            null,
403            1.0,  // Definite - known function output column
404            "known_function_column"
405        );
406        columnSources.put(columnName, source);
407        noteColumnSourceAdded(columnName);
408    }
409
410    @Override
411    public ColumnLevel hasColumn(String columnName) {
412        ensureValidated();
413
414        // Check if column exists in known columns (including inferred ones).
415        // Canonical-key indexed lookup (Mantis 4684): the previous matcher loop
416        // over the inferred-column map made wide select lists O(N²), with a
417        // collator comparison per entry on SQL Server.
418        if (findColumnKeyByMatcher(columnName) != null) {
419            return ColumnLevel.EXISTS;
420        }
421
422        // If we don't have metadata, any column MAYBE exists
423        // (we can't definitively say it doesn't exist)
424        if (!hasMetadata) {
425            return ColumnLevel.MAYBE;
426        }
427
428        return ColumnLevel.NOT_EXISTS;
429    }
430
431    @Override
432    public ColumnSource resolveColumn(String columnName) {
433        ensureValidated();
434
435        // First try to find in known columns
436        String matchedKey = findColumnKeyByMatcher(columnName);
437        ColumnSource existing = matchedKey == null ? null : columnSources.get(matchedKey);
438        if (existing != null) {
439            return existing;
440        }
441
442        // If no metadata available, create an inferred ColumnSource
443        // This allows columns to be resolved against tables without metadata
444        // Note: hasMetadata is false when we don't have DDL or SQLEnv metadata
445        if (!hasMetadata && columnName != null && !columnName.isEmpty()) {
446            // Create a column source with moderate confidence
447            // Since we don't have metadata, we're inferring this column exists
448            ColumnSource inferred = new ColumnSource(
449                this,
450                columnName,
451                null,  // No definition node
452                0.8,   // Moderate confidence - we don't have proof this column exists
453                "inferred_from_usage"
454            );
455
456            // Cache it for future lookups
457            columnSources.put(columnName, inferred);
458            noteColumnSourceAdded(columnName);
459
460            return inferred;
461        }
462
463        return null;
464    }
465
466    public TTable getTable() {
467        return table;
468    }
469
470    /**
471     * Returns true if this namespace has actual metadata (from DDL or SQLEnv).
472     * When false, column resolution will infer columns from usage.
473     *
474     * @return true if metadata is available, false if columns are inferred
475     */
476    public boolean hasMetadata() {
477        ensureValidated();
478        return hasMetadata;
479    }
480
481    /**
482     * Slice S4: authoritative metadata-state tri-value (plan §5.5).
483     *
484     * <p>Distinguishes:</p>
485     * <ul>
486     *   <li>{@link MetadataState#FOUND} — DDL columns present, or
487     *       {@link TSQLEnv} returned a table with a non-empty column list, or
488     *       a known table-valued function (e.g. STRING_SPLIT) populated its
489     *       columns.</li>
490     *   <li>{@link MetadataState#NOT_FOUND_IN_CATALOG} — a non-empty
491     *       {@code TSQLEnv} was consulted but the table itself is absent.
492     *       Never used when no environment has been configured.</li>
493     *   <li>{@link MetadataState#METADATA_UNAVAILABLE} — no environment, an
494     *       empty environment (no catalogs registered), the lookup returned a
495     *       table without expanded columns (Q8 view-without-columns), or any
496     *       other unauthoritative state.</li>
497     * </ul>
498     */
499    @Override
500    public MetadataState getMetadataState() {
501        ensureValidated();
502
503        // Authoritative: DDL columns or TSQLEnv-resolved table with columns — unless two
504        // generated outputs collapsed onto one name, in which case the map is not the schema.
505        if (hasMetadata && columnSources != null && !columnSources.isEmpty() && !generatedNameCollision) {
506            return MetadataState.FOUND;
507        }
508
509        // Found-but-unexpanded: TSQLEnv returned the table but no columns came
510        // through. Q8 view-without-columns falls here. Never NOT_FOUND_IN_CATALOG
511        // and never UNKNOWN_COLUMN — strict mode may emit a WARNING in S6.
512        if (resolvedTable != null) {
513            return MetadataState.METADATA_UNAVAILABLE;
514        }
515
516        // Lookup ran against a populated environment and missed entirely. The
517        // env has at least one catalog registered, so we trust the absence as
518        // an authoritative table-not-found signal. Bridges pre-register an
519        // empty default catalog/schema during construction (see
520        // CatalogRuntimeToSQLEnvBridge.applyDefaults), which preserves this
521        // behavior under lazy materialization.
522        if (sqlEnv != null && hasAnyCatalogContent(sqlEnv)) {
523            return MetadataState.NOT_FOUND_IN_CATALOG;
524        }
525
526        return MetadataState.METADATA_UNAVAILABLE;
527    }
528
529    private static boolean hasAnyCatalogContent(TSQLEnv env) {
530        if (env == null) {
531            return false;
532        }
533        java.util.List<TSQLCatalog> catalogs = env.getCatalogList();
534        return catalogs != null && !catalogs.isEmpty();
535    }
536
537    /**
538     * Add a USING clause column to this table's namespace.
539     * This is called during scope building when a JOIN...USING clause
540     * includes columns from this table. The USING column exists in BOTH
541     * tables of the join.
542     *
543     * @param columnName the name of the USING column
544     */
545    public void addUsingColumn(String columnName) {
546        ensureValidated();
547
548        // Only add if not already present
549        if (hasColumn(columnName) == ColumnLevel.EXISTS) {
550            return;
551        }
552
553        ColumnSource source = new ColumnSource(
554            this,
555            columnName,
556            null,  // No definition node
557            1.0,   // Definite - USING clause semantics guarantee the column exists
558            "using_clause_column"
559        );
560        columnSources.put(columnName, source);
561        noteColumnSourceAdded(columnName);
562    }
563
564    @Override
565    public String toString() {
566        return "TableNamespace(" + getDisplayName() + ", columns=" +
567               (columnSources != null ? columnSources.size() : "?") + ")";
568    }
569}