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