001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.util.SQLUtil;
006import gudusoft.gsqlparser.nodes.TTable;
007import gudusoft.gsqlparser.nodes.TColumnDefinition;
008import gudusoft.gsqlparser.nodes.TColumnDefinitionList;
009import gudusoft.gsqlparser.nodes.TExpression;
010import gudusoft.gsqlparser.nodes.TFunctionCall;
011import gudusoft.gsqlparser.nodes.TObjectName;
012import gudusoft.gsqlparser.nodes.TObjectNameList;
013import gudusoft.gsqlparser.resolver2.ColumnLevel;
014import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
015import gudusoft.gsqlparser.resolver2.model.ColumnSource;
016import gudusoft.gsqlparser.resolver2.model.QualifiedName;
017import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver;
018import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
019import gudusoft.gsqlparser.sqlenv.TSQLEnv;
020import gudusoft.gsqlparser.sqlenv.TSQLTable;
021import gudusoft.gsqlparser.sqlenv.TSQLColumn;
022
023import java.util.LinkedHashMap;
024import java.util.Map;
025
026/**
027 * Namespace representing a physical table.
028 * Provides column information from table metadata.
029 *
030 * Column sources come from:
031 * 1. Table metadata (if available from DDL)
032 * 2. External metadata providers (SQLEnv)
033 * 3. Inferred from usage (when no metadata available)
034 */
035public class TableNamespace extends AbstractNamespace {
036
037    private final TTable table;
038
039    /** TSQLEnv for looking up table metadata */
040    private TSQLEnv sqlEnv;
041
042    /** Database vendor for qualified name resolution */
043    private EDbVendor vendor;
044
045    /** Qualified name resolver for normalizing table references */
046    private QualifiedNameResolver qualifiedNameResolver;
047
048    /** The qualified name of this table (with defaults applied) */
049    private QualifiedName qualifiedName;
050
051    /** Flag to track if this table has actual metadata (vs. only inferred columns) */
052    private boolean hasMetadata = false;
053
054    /** The resolved TSQLTable from SQLEnv (if found) */
055    private TSQLTable resolvedTable;
056
057    public TableNamespace(TTable table, INameMatcher nameMatcher) {
058        super(table, nameMatcher);
059        this.table = table;
060    }
061
062    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv) {
063        super(table, nameMatcher);
064        this.table = table;
065        this.sqlEnv = sqlEnv;
066        // Default vendor - use setVendor() to override
067        if (sqlEnv != null) {
068            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, EDbVendor.dbvoracle);
069        }
070    }
071
072    /**
073     * Create a TableNamespace with full qualified name resolution support.
074     *
075     * @param table The table AST node
076     * @param nameMatcher The name matcher for case sensitivity
077     * @param sqlEnv The SQL environment for metadata lookup
078     * @param vendor The database vendor
079     */
080    public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv, EDbVendor vendor) {
081        super(table, nameMatcher);
082        this.table = table;
083        this.sqlEnv = sqlEnv;
084        this.vendor = vendor;
085        if (sqlEnv != null) {
086            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
087        }
088    }
089
090    public TableNamespace(TTable table) {
091        super(table);
092        this.table = table;
093    }
094
095    /**
096     * Set the TSQLEnv for metadata lookup.
097     * @param sqlEnv the SQL environment containing table metadata
098     */
099    public void setSqlEnv(TSQLEnv sqlEnv) {
100        this.sqlEnv = sqlEnv;
101        if (sqlEnv != null && vendor != null) {
102            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
103        }
104        // Reset cached qualified name so it's recomputed with new env
105        this.qualifiedName = null;
106    }
107
108    /**
109     * Get the TSQLEnv used for metadata lookup.
110     * @return the SQL environment, or null if not set
111     */
112    public TSQLEnv getSqlEnv() {
113        return sqlEnv;
114    }
115
116    /**
117     * Set the database vendor.
118     * @param vendor the database vendor
119     */
120    public void setVendor(EDbVendor vendor) {
121        this.vendor = vendor;
122        if (sqlEnv != null) {
123            this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor);
124        }
125        // Reset cached qualified name
126        this.qualifiedName = null;
127    }
128
129    /**
130     * Get the database vendor.
131     * @return the database vendor, or null if not set
132     */
133    public EDbVendor getVendor() {
134        return vendor;
135    }
136
137    /**
138     * Get the qualified name resolver.
139     * @return the resolver, or null if sqlEnv is not set
140     */
141    public QualifiedNameResolver getQualifiedNameResolver() {
142        return qualifiedNameResolver;
143    }
144
145    /**
146     * Get the fully qualified name of this table.
147     *
148     * <p>The qualified name is computed by applying defaults from TSQLEnv
149     * to the table's partial name.
150     *
151     * @return the qualified name, or null if table name is unavailable
152     */
153    public QualifiedName getQualifiedName() {
154        if (qualifiedName != null) {
155            return qualifiedName;
156        }
157
158        TObjectName tableName = table.getTableName();
159        if (tableName == null) {
160            return null;
161        }
162
163        if (qualifiedNameResolver != null) {
164            qualifiedName = qualifiedNameResolver.resolve(tableName);
165        } else {
166            // Fallback: build from table name parts without defaults
167            String catalog = tableName.getDatabaseString();
168            String schema = tableName.getSchemaString();
169            String name = tableName.getObjectString();
170            if (name == null || name.isEmpty()) {
171                name = tableName.toString();
172            }
173            if (name != null && !name.isEmpty()) {
174                qualifiedName = QualifiedName.forTable(catalog, schema, name);
175            }
176        }
177
178        return qualifiedName;
179    }
180
181    /**
182     * Get the resolved TSQLTable from SQLEnv.
183     * @return the resolved table, or null if not found in SQLEnv
184     */
185    public TSQLTable getResolvedTable() {
186        return resolvedTable;
187    }
188
189    @Override
190    public String getDisplayName() {
191        String alias = table.getAliasName();
192        if (alias != null && !alias.isEmpty()) {
193            return alias;
194        }
195        String fullName = table.getFullName();
196        if (fullName != null && !fullName.isEmpty()) {
197            return fullName;
198        }
199        // Fallback to simple table name
200        String name = table.getName();
201        if (name != null && !name.isEmpty()) {
202            return name;
203        }
204        // Last resort: try table name object
205        TObjectName tableName = table.getTableName();
206        if (tableName != null) {
207            String tableStr = tableName.getTableString();
208            if (tableStr != null && !tableStr.isEmpty()) {
209                return tableStr;
210            }
211            String tableNameStr = tableName.toString();
212            if (tableNameStr != null && !tableNameStr.isEmpty()) {
213                return tableNameStr;
214            }
215        }
216        return "";
217    }
218
219    @Override
220    public TTable getFinalTable() {
221        return table;
222    }
223
224    @Override
225    public TTable getSourceTable() {
226        return table;
227    }
228
229    @Override
230    protected void doValidate() {
231        // Load columns from metadata
232        columnSources = new LinkedHashMap<>();
233        hasMetadata = false;
234        resolvedTable = null;
235
236        // Priority 1: Check if table has column list (from DDL like CREATE TABLE)
237        TColumnDefinitionList columnDefs = table.getColumnDefinitions();
238        if (columnDefs != null && columnDefs.size() > 0) {
239            // Has explicit column list from CREATE TABLE or metadata
240            hasMetadata = true;
241            for (int i = 0; i < columnDefs.size(); i++) {
242                TColumnDefinition colDef = columnDefs.getColumn(i);
243                TObjectName colNameObj = colDef.getColumnName();
244                if (colNameObj == null) continue;
245
246                String colName = colNameObj.toString();
247
248                ColumnSource source = new ColumnSource(
249                    this,
250                    colName,
251                    colDef,
252                    1.0,  // Definite - from DDL metadata
253                    "ddl_metadata"
254                );
255                columnSources.put(colName, source);
256            }
257            return; // DDL metadata takes priority
258        }
259
260        // Priority 2: Try to load from TSQLEnv if available
261        if (sqlEnv != null) {
262            TObjectName tableName = table.getTableName();
263            TSQLTable sqlTable = null;
264            if (tableName != null) {
265                sqlTable = sqlEnv.searchTable(tableName);
266            }
267
268            if (sqlTable != null) {
269                // Found table in SQLEnv - load all columns
270                resolvedTable = sqlTable;
271                hasMetadata = true;
272
273                for (TSQLColumn column : sqlTable.getColumnList()) {
274                    String colName = column.getName();
275
276                    ColumnSource source = new ColumnSource(
277                        this,
278                        colName,
279                        null,  // No TColumnDefinition node from SQLEnv
280                        1.0,   // Definite - from SQLEnv metadata
281                        "sqlenv_metadata"
282                    );
283                    columnSources.put(colName, source);
284                }
285                return; // SQLEnv metadata found
286            }
287        }
288
289        // Priority 3: Table-valued functions.
290        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) {
291            String[] knownColumns = knownTableFunctionColumns();
292            TObjectNameList aliasColumns = functionAliasColumns();
293
294            // 3a: an explicit column alias list renames the function's output columns
295            //     POSITIONALLY, e.g. FROM TABLE(FLATTEN(input => t.c)) f(s, k).
296            if (aliasColumns != null) {
297                for (int i = 0; i < aliasColumns.size(); i++) {
298                    TObjectName colName = aliasColumns.getObjectName(i);
299                    if (colName != null) {
300                        addKnownFunctionColumn(colName.toString());
301                    }
302                }
303                if (knownColumns != null) {
304                    // A partial list renames only the leading columns; the rest keep
305                    // their built-in names, so the exposed set is still fully known.
306                    for (int i = aliasColumns.size(); i < knownColumns.length; i++) {
307                        addKnownFunctionColumn(knownColumns[i]);
308                    }
309                    hasMetadata = true;
310                } else {
311                    // The function's own output schema is unknown, so a partial list
312                    // leaves unnamed columns that keep names we cannot enumerate.
313                    // Stay non-authoritative: those must remain inferable rather than
314                    // being reported as non-existent.
315                    hasMetadata = false;
316                }
317                return;
318            }
319
320            // 3b: known table-valued functions with well-defined output columns
321            if (knownColumns != null) {
322                hasMetadata = true;
323                for (String knownColumn : knownColumns) {
324                    addKnownFunctionColumn(knownColumn);
325                }
326                return;
327            }
328        }
329
330        // Priority 4: No metadata available
331        // For tables without metadata, we'll handle this as MAYBE in hasColumn
332        // Columns will be inferred from usage in resolveColumn()
333    }
334
335    /** SQL Server STRING_SPLIT output columns (ordinal is SQL Server 2022+). */
336    private static final String[] STRING_SPLIT_COLUMNS = {"value", "ordinal"};
337
338    /**
339     * Snowflake FLATTEN output columns. FLATTEN is a built-in table function with a
340     * fixed, documented output schema:
341     * https://docs.snowflake.com/en/sql-reference/functions/flatten
342     */
343    private static final String[] SNOWFLAKE_FLATTEN_COLUMNS =
344        {"SEQ", "KEY", "PATH", "INDEX", "VALUE", "THIS"};
345
346    /**
347     * Snowflake SPLIT_TO_TABLE output columns — same fixed-schema shape as FLATTEN:
348     * https://docs.snowflake.com/en/sql-reference/functions/split_to_table
349     */
350    private static final String[] SNOWFLAKE_SPLIT_TO_TABLE_COLUMNS = {"SEQ", "INDEX", "VALUE"};
351
352    /**
353     * The explicit column alias list of this table, or null when there is none.
354     *
355     * <p>Snowflake, PostgreSQL and Presto/Trino all allow renaming a table
356     * function's output columns: {@code FROM f(...) alias (c1, c2, ...)}. The list
357     * may be shorter than the function's output — the remaining columns then keep
358     * their original names.</p>
359     */
360    private TObjectNameList functionAliasColumns() {
361        if (table.getAliasClause() == null) {
362            return null;
363        }
364        TObjectNameList columns = table.getAliasClause().getColumns();
365        if (columns == null || columns.size() == 0) {
366            return null;
367        }
368        return columns;
369    }
370
371    /**
372     * Output columns of a built-in table-valued function whose schema is fixed by
373     * the vendor, or null when this function has no known schema (the caller then
374     * falls back to inferring columns from usage).
375     *
376     * <p>Only an unqualified function name is matched: a schema-qualified call such
377     * as {@code myschema.flatten(...)} is a user-defined function that merely shares
378     * the name, and must keep the inferring behaviour.</p>
379     */
380    private String[] knownTableFunctionColumns() {
381        EDbVendor tableVendor = getTableVendor();
382
383        if (tableVendor == EDbVendor.dbvsnowflake) {
384            // Mantis 4663: an unqualified reference to a FLATTEN output column (VALUE,
385            // SEQ, ...) must resolve to the FLATTEN table rather than being left
386            // ambiguous against the other FROM-clause items. Snowflake spells these
387            // built-ins both as LATERAL f(...) and as TABLE(f(...)), so the TABLE
388            // wrapper is transparent here.
389            String snowflakeName = getUnqualifiedTableFunctionName(true);
390            if (snowflakeName != null && !snowflakeName.isEmpty()) {
391                if (SQLUtil.sameName(tableVendor, ESQLDataObjectType.dotFunction,
392                                     snowflakeName, "FLATTEN")) {
393                    return SNOWFLAKE_FLATTEN_COLUMNS;
394                }
395                if (SQLUtil.sameName(tableVendor, ESQLDataObjectType.dotFunction,
396                                     snowflakeName, "SPLIT_TO_TABLE")) {
397                    return SNOWFLAKE_SPLIT_TO_TABLE_COLUMNS;
398                }
399            }
400        }
401
402        // STRING_SPLIT is matched on the directly named function only. Looking through
403        // a TABLE(...) wrapper here would newly claim SQL Server's schema for a
404        // same-named user-defined table function in another dialect, whose real output
405        // columns would then be reported as non-existent.
406        String directName = getUnqualifiedTableFunctionName(false);
407        if (directName != null && !directName.isEmpty()
408            && SQLUtil.sameName(getTableVendor(), ESQLDataObjectType.dotFunction,
409                                directName, "STRING_SPLIT")) {
410            return STRING_SPLIT_COLUMNS;
411        }
412
413        // Add more known table functions here as needed
414        // e.g., OPENJSON, OPENXML, etc.
415        return null;
416    }
417
418    /**
419     * The vendor of the parsed statement this table belongs to. Prefers the vendor
420     * the namespace was constructed with and falls back to the AST node's own
421     * vendor, because {@link gudusoft.gsqlparser.resolver2.ScopeBuilder} builds
422     * table namespaces through constructors that do not carry the vendor.
423     */
424    private EDbVendor getTableVendor() {
425        if (vendor != null) {
426            return vendor;
427        }
428        return table.dbvendor;
429    }
430
431    /**
432     * Get the function name for table-valued functions, but only when the call is
433     * unqualified. Returns null for a schema/database-qualified call.
434     *
435     * @param lookThroughTableKeyword when true, an explicit {@code TABLE(...)} wrapper
436     *        is transparent: Snowflake spells the same table function both ways —
437     *        {@code LATERAL FLATTEN(input => c)} and {@code TABLE(FLATTEN(input => c))}
438     *        — and both expose the same output columns.
439     */
440    private String getUnqualifiedTableFunctionName(boolean lookThroughTableKeyword) {
441        if (lookThroughTableKeyword) {
442            TFunctionCall unwrapped = unwrapTableKeywordFunction(table.getFuncCall());
443            if (unwrapped != null && unwrapped != table.getFuncCall()
444                && unwrapped.getFunctionName() != null) {
445                TObjectName funcName = unwrapped.getFunctionName();
446                return isQualified(funcName) ? null : funcName.toString();
447            }
448        }
449
450        TObjectName tableName = table.getTableName();
451        if (tableName != null) {
452            if (isQualified(tableName)) {
453                return null;
454            }
455            return tableName.toString();
456        }
457        if (table.getFuncCall() != null && table.getFuncCall().getFunctionName() != null) {
458            TObjectName funcName = table.getFuncCall().getFunctionName();
459            if (isQualified(funcName)) {
460                return null;
461            }
462            return funcName.toString();
463        }
464        return table.getName();
465    }
466
467    /**
468     * Unwrap {@code TABLE(f(...))} to the inner call {@code f(...)}, or return the
469     * call unchanged when it is not such a wrapper. Only a single-argument
470     * {@code TABLE(...)} whose sole argument is itself a function call is unwrapped,
471     * so a genuine user function named TABLE with other arguments is left alone.
472     */
473    private TFunctionCall unwrapTableKeywordFunction(TFunctionCall funcCall) {
474        if (funcCall == null || funcCall.getFunctionName() == null) {
475            return funcCall;
476        }
477        if (isQualified(funcCall.getFunctionName())) {
478            return funcCall;
479        }
480        if (!SQLUtil.sameName(getTableVendor(), ESQLDataObjectType.dotFunction,
481                              funcCall.getFunctionName().toString(), "TABLE")) {
482            return funcCall;
483        }
484        if (funcCall.getArgs() == null || funcCall.getArgs().size() != 1) {
485            return funcCall;
486        }
487        TExpression arg = funcCall.getArgs().getExpression(0);
488        if (arg == null || arg.getFunctionCall() == null) {
489            return funcCall;
490        }
491        return arg.getFunctionCall();
492    }
493
494    private static boolean isQualified(TObjectName name) {
495        String schema = name.getSchemaString();
496        if (schema != null && !schema.isEmpty()) {
497            return true;
498        }
499        String database = name.getDatabaseString();
500        return database != null && !database.isEmpty();
501    }
502
503
504    /**
505     * Add a known column for a table-valued function.
506     */
507    private void addKnownFunctionColumn(String columnName) {
508        ColumnSource source = new ColumnSource(
509            this,
510            columnName,
511            null,
512            1.0,  // Definite - known function output column
513            "known_function_column"
514        );
515        columnSources.put(columnName, source);
516        noteColumnSourceAdded(columnName);
517    }
518
519    @Override
520    public ColumnLevel hasColumn(String columnName) {
521        ensureValidated();
522
523        // Check if column exists in known columns (including inferred ones).
524        // Canonical-key indexed lookup (Mantis 4684): the previous matcher loop
525        // over the inferred-column map made wide select lists O(N²), with a
526        // collator comparison per entry on SQL Server.
527        if (findColumnKeyByMatcher(columnName) != null) {
528            return ColumnLevel.EXISTS;
529        }
530
531        // If we don't have metadata, any column MAYBE exists
532        // (we can't definitively say it doesn't exist)
533        if (!hasMetadata) {
534            return ColumnLevel.MAYBE;
535        }
536
537        return ColumnLevel.NOT_EXISTS;
538    }
539
540    @Override
541    public ColumnSource resolveColumn(String columnName) {
542        ensureValidated();
543
544        // First try to find in known columns
545        ColumnSource existing = super.resolveColumn(columnName);
546        if (existing != null) {
547            return existing;
548        }
549
550        // If no metadata available, create an inferred ColumnSource
551        // This allows columns to be resolved against tables without metadata
552        // Note: hasMetadata is false when we don't have DDL or SQLEnv metadata
553        if (!hasMetadata && columnName != null && !columnName.isEmpty()) {
554            // Create a column source with moderate confidence
555            // Since we don't have metadata, we're inferring this column exists
556            ColumnSource inferred = new ColumnSource(
557                this,
558                columnName,
559                null,  // No definition node
560                0.8,   // Moderate confidence - we don't have proof this column exists
561                "inferred_from_usage"
562            );
563
564            // Cache it for future lookups
565            columnSources.put(columnName, inferred);
566            noteColumnSourceAdded(columnName);
567
568            return inferred;
569        }
570
571        return null;
572    }
573
574    public TTable getTable() {
575        return table;
576    }
577
578    /**
579     * Returns true if this namespace has actual metadata (from DDL or SQLEnv).
580     * When false, column resolution will infer columns from usage.
581     *
582     * @return true if metadata is available, false if columns are inferred
583     */
584    public boolean hasMetadata() {
585        ensureValidated();
586        return hasMetadata;
587    }
588
589    /**
590     * Slice S4: authoritative metadata-state tri-value (plan §5.5).
591     *
592     * <p>Distinguishes:</p>
593     * <ul>
594     *   <li>{@link MetadataState#FOUND} — DDL columns present, or
595     *       {@link TSQLEnv} returned a table with a non-empty column list, or
596     *       a known table-valued function (e.g. STRING_SPLIT) populated its
597     *       columns.</li>
598     *   <li>{@link MetadataState#NOT_FOUND_IN_CATALOG} — a non-empty
599     *       {@code TSQLEnv} was consulted but the table itself is absent.
600     *       Never used when no environment has been configured.</li>
601     *   <li>{@link MetadataState#METADATA_UNAVAILABLE} — no environment, an
602     *       empty environment (no catalogs registered), the lookup returned a
603     *       table without expanded columns (Q8 view-without-columns), or any
604     *       other unauthoritative state.</li>
605     * </ul>
606     */
607    @Override
608    public MetadataState getMetadataState() {
609        ensureValidated();
610
611        // Authoritative: DDL columns or TSQLEnv-resolved table with columns.
612        if (hasMetadata && columnSources != null && !columnSources.isEmpty()) {
613            return MetadataState.FOUND;
614        }
615
616        // Found-but-unexpanded: TSQLEnv returned the table but no columns came
617        // through. Q8 view-without-columns falls here. Never NOT_FOUND_IN_CATALOG
618        // and never UNKNOWN_COLUMN — strict mode may emit a WARNING in S6.
619        if (resolvedTable != null) {
620            return MetadataState.METADATA_UNAVAILABLE;
621        }
622
623        // Lookup ran against a populated environment and missed entirely. The
624        // env has at least one catalog registered, so we trust the absence as
625        // an authoritative table-not-found signal. Bridges pre-register an
626        // empty default catalog/schema during construction (see
627        // CatalogRuntimeToSQLEnvBridge.applyDefaults), which preserves this
628        // behavior under lazy materialization.
629        if (sqlEnv != null && hasAnyCatalogContent(sqlEnv)) {
630            return MetadataState.NOT_FOUND_IN_CATALOG;
631        }
632
633        return MetadataState.METADATA_UNAVAILABLE;
634    }
635
636    private static boolean hasAnyCatalogContent(TSQLEnv env) {
637        if (env == null) {
638            return false;
639        }
640        java.util.List<TSQLCatalog> catalogs = env.getCatalogList();
641        return catalogs != null && !catalogs.isEmpty();
642    }
643
644    /**
645     * Add a USING clause column to this table's namespace.
646     * This is called during scope building when a JOIN...USING clause
647     * includes columns from this table. The USING column exists in BOTH
648     * tables of the join.
649     *
650     * @param columnName the name of the USING column
651     */
652    public void addUsingColumn(String columnName) {
653        ensureValidated();
654
655        // Only add if not already present
656        if (hasColumn(columnName) == ColumnLevel.EXISTS) {
657            return;
658        }
659
660        ColumnSource source = new ColumnSource(
661            this,
662            columnName,
663            null,  // No definition node
664            1.0,   // Definite - USING clause semantics guarantee the column exists
665            "using_clause_column"
666        );
667        columnSources.put(columnName, source);
668        noteColumnSourceAdded(columnName);
669    }
670
671    @Override
672    public String toString() {
673        return "TableNamespace(" + getDisplayName() + ", columns=" +
674               (columnSources != null ? columnSources.size() : "?") + ")";
675    }
676}