001package gudusoft.gsqlparser.resolver;
002
003import gudusoft.gsqlparser.*;
004import gudusoft.gsqlparser.compiler.TContext;
005import gudusoft.gsqlparser.compiler.TScope;
006import gudusoft.gsqlparser.nodes.*;
007import gudusoft.gsqlparser.sqlenv.TSQLEnv;
008import gudusoft.gsqlparser.sqlenv.TSQLFunction;
009import gudusoft.gsqlparser.sqlenv.TSQLTable;
010import gudusoft.gsqlparser.stmt.TCreateFunctionStmt;
011import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
012import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
013
014import java.util.Stack;
015
016/**
017 * TMetadataCollector is responsible for collecting metadata information from SQL statements,
018 * particularly focusing on table and function definitions. It traverses the parse tree
019 * and builds up metadata in the global scope.
020 */
021public class TMetadataCollector extends TParseTreeVisitor {
022    private TContext globalScope;        // Stores global context including SQL environment
023    private TStatementList sqlStatements; // List of SQL statements to analyze
024    private Stack<TScope> scopeStack = new Stack<>();  // Maintains scope hierarchy
025    
026    /**
027     * Constructor for TMetadataCollector.
028     * Initializes the collector with a list of SQL statements and a global context.
029     *
030     * @param sqls List of SQL statements to analyze
031     * @param context Global context that will store collected metadata
032     */
033    public TMetadataCollector(TStatementList sqls, TContext context) {
034        this.globalScope = context;
035        sqlStatements = sqls;
036        scopeStack.push(globalScope);
037    }
038
039    /**
040     * Main method to start metadata collection process.
041     * Iterates through all SQL statements and collects metadata from each one.
042     * This includes:
043     * - Table definitions from CREATE TABLE statements
044     * - Function definitions from CREATE FUNCTION statements
045     * - Other metadata from various SQL statements
046     */
047    public void collect() {
048        for (int i = 0; i < sqlStatements.size(); i++) {
049            sqlStatements.get(i).acceptChildren(this);
050        }
051    }
052
053    /**
054     * Processes CREATE TABLE statements to collect table metadata.
055     * Handles both regular table creation and CREATE TABLE AS SELECT.
056     *
057     * @param stmt The CREATE TABLE statement to process
058     *
059     * Actions performed:
060     * 1. Creates new table entry in SQL environment
061     * 2. Sets table name and resolves it
062     * 3. Processes column definitions
063     * 4. Handles subquery-based table creation
064     */
065    /** Register only a schema declared by this CREATE, never an unknown LIKE/CLONE shape. */
066    public static TSQLTable registerDeclaredTable(TCreateTableSqlStatement stmt, TSQLEnv env) {
067        return env == null ? null
068                : registerDeclaredTable(stmt, env, env.getDefaultCatalogName(), env.getDefaultSchemaName());
069    }
070
071    /**
072     * {@link #registerDeclaredTable(TCreateTableSqlStatement, TSQLEnv)} as if {@code env}'s current database and schema
073     * were {@code defaultCatalog} and {@code defaultSchema}, without changing them.
074     */
075    public static TSQLTable registerDeclaredTable(TCreateTableSqlStatement stmt, TSQLEnv env, String defaultCatalog,
076            String defaultSchema) {
077        TSQLTable table = addDeclaredTable(stmt, env, defaultCatalog, defaultSchema);
078        if (table == null) return null;
079        stmt.getTargetTable().setResolvedTable(table);
080        stmt.getTargetTable().getTableName().setSqlEnv(env);
081        return table;
082    }
083
084    /**
085     * The catalog half of {@link #registerDeclaredTable}: put the declared table in {@code env} and return it, leaving
086     * the statement as it is. A table {@code env} already holds there is kept, unless the statement replaces it.
087     */
088    public static TSQLTable addDeclaredTable(TCreateTableSqlStatement stmt, TSQLEnv env) {
089        return env == null ? null : addDeclaredTable(stmt, env, env.getDefaultCatalogName(), env.getDefaultSchemaName());
090    }
091
092    /** {@link #addDeclaredTable(TCreateTableSqlStatement, TSQLEnv)} in an explicit database and schema. */
093    public static TSQLTable addDeclaredTable(TCreateTableSqlStatement stmt, TSQLEnv env, String defaultCatalog,
094            String defaultSchema) {
095        if (!hasDeclaredShape(stmt, env)) return null;
096        TObjectName name = stmt.getTableName();
097        gudusoft.gsqlparser.sqlenv.TSQLSchema owner = declaredTableOwner(name, stmt.dbvendor, env, defaultCatalog,
098                defaultSchema);
099        TSQLTable table = owner.findTable(name.getObjectString());
100        if (table == null || replacesTable(stmt)) {
101            table = new TSQLTable(owner, name.getObjectString());
102            if (stmt.getColumnList() != null) for (TColumnDefinition column : stmt.getColumnList()) {
103                if (column.getColumnName() != null) table.addColumn(column.getColumnName().toString(), column.getDatatype());
104            }
105        }
106        return table;
107    }
108
109    /**
110     * The schema a table named {@code name} is declared in: its own qualification, else {@code defaultCatalog} and
111     * {@code defaultSchema}, else DEFAULT (dbo for SQL Server). Creates the catalog and schema when missing.
112     */
113    public static gudusoft.gsqlparser.sqlenv.TSQLSchema declaredTableOwner(TObjectName name, EDbVendor vendor,
114            TSQLEnv env, String defaultCatalog, String defaultSchema) {
115        return env.createSQLCatalog(declaredCatalog(name, defaultCatalog))
116                .createSchema(declaredSchema(name, vendor, defaultSchema));
117    }
118
119    /**
120     * The table {@code env} already holds where a declaration of {@code stmt}'s table would go if {@code env}'s current
121     * database and schema were {@code defaultCatalog} and {@code defaultSchema}, or null. Creates no catalog or schema.
122     */
123    public static TSQLTable findDeclaredTable(TCreateTableSqlStatement stmt, TSQLEnv env, String defaultCatalog,
124            String defaultSchema) {
125        if (env == null || !env.isEnableGetMetadataFromDDL() || stmt.getTableName() == null) return null;
126        TObjectName name = stmt.getTableName();
127        gudusoft.gsqlparser.sqlenv.TSQLCatalog catalog = env.searchCatalog(declaredCatalog(name, defaultCatalog));
128        gudusoft.gsqlparser.sqlenv.TSQLSchema owner = catalog == null ? null
129                : catalog.getSchema(declaredSchema(name, stmt.dbvendor, defaultSchema), false);
130        return owner == null ? null : owner.findTable(name.getObjectString());
131    }
132
133    /** CREATE OR REPLACE without IF NOT EXISTS: the declaration takes the place of a table of that name. */
134    public static boolean replacesTable(TCreateTableSqlStatement stmt) {
135        if (stmt.isIfNotExists()) return false;
136        for (int i = 0; i < stmt.sourcetokenlist.size(); i++) {
137            TSourceToken token = stmt.sourcetokenlist.get(i);
138            if (token.tokencode == TBaseType.rrw_table) break;
139            if (token.tokencode == TBaseType.rrw_replace) return true;
140        }
141        return false;
142    }
143
144    private static boolean hasDeclaredShape(TCreateTableSqlStatement stmt, TSQLEnv env) {
145        if (env == null || !env.isEnableGetMetadataFromDDL() || stmt.getTargetTable() == null) return false;
146        boolean explicit = stmt.getColumnList() != null && stmt.getColumnList().size() > 0;
147        return explicit || stmt.getTableSourceType() == TCreateTableSqlStatement.TableSourceType.normal;
148    }
149
150    private static String declaredCatalog(TObjectName name, String defaultCatalog) {
151        String catalog = name.getDatabaseString();
152        if (catalog == null || catalog.isEmpty()) catalog = defaultCatalog;
153        return catalog == null ? "DEFAULT" : catalog;
154    }
155
156    private static String declaredSchema(TObjectName name, EDbVendor vendor, String defaultSchema) {
157        String schema = name.getSchemaString();
158        if (schema == null || schema.isEmpty()) schema = defaultSchema;
159        if (schema != null) return schema;
160        return (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) ? "dbo" : "DEFAULT";
161    }
162
163    public void preVisit(TCreateTableSqlStatement stmt) {
164        if (stmt.getColumnList().size() > 0 || stmt.getTableSourceType() == TCreateTableSqlStatement.TableSourceType.normal) {
165            registerDeclaredTable(stmt, globalScope.getSqlEnv());
166            return;
167        }
168        if (stmt.getTableSourceType() != TCreateTableSqlStatement.TableSourceType.subquery) return;
169
170        TSQLTable sqlTable = null;
171        TTable createTable = stmt.getTargetTable();
172        if (globalScope.getSqlEnv() == null) return;
173
174        // Create new table entry
175        if (stmt.getTargetTable() != null) {
176            sqlTable = globalScope.getSqlEnv().addTable(createTable.getFullName(), true);
177        }
178        if (sqlTable == null) return;
179        createTable.setResolvedTable(sqlTable);
180
181        // Set SQL environment for table name
182        createTable.getTableName().setSqlEnv(globalScope.getSqlEnv());
183
184        // Process columns
185        if (stmt.getColumnList().size() != 0) {
186            // Handle explicit column definitions
187            for(TColumnDefinition cd : stmt.getColumnList()) {
188                sqlTable.addColumn(cd.getColumnName().toString(), cd.getDatatype());
189            }
190        } else if (stmt.getSubQuery() != null) {
191            // Handle CREATE TABLE AS SELECT
192            TSelectSqlStatement viewQuery = stmt.getSubQuery();
193            if (viewQuery.isCombinedQuery()) {
194                viewQuery = viewQuery.getRightStmt();
195            }
196
197            TResultColumnList resultColumnList = stmt.getSubQuery().getResultColumnList();
198            if (resultColumnList != null) {
199                for (TResultColumn resultColumn : resultColumnList) {
200                    if (resultColumn.getAliasNameList().size() > 0){
201                        // select t.a1, t.a2, stack(2, 'T', t.a1, t.a2, t.a3/t.a4, t.a5/t.a6,   'T+0', t.a7, t.a8, t.a9/t.a10, t.a11/t.a12) as (b1, b2, b3, b4, b5)  from db1.table1 t
202                        for(int j = 0; j < resultColumn.getAliasNameList().size(); j++){
203                            TObjectName column = resultColumn.getAliasNameList().get(j);
204                            sqlTable.addColumn(TSQLEnv.getObjectName( TBaseType.removeQuoteChar(column.toString())));
205                            createTable.getLinkedColumns().addObjectName(column); // for getTableColumn work correctly
206                        }
207
208                    }else{
209                        sqlTable.addColumn(TSQLEnv.getObjectName(resultColumn.getDisplayName()));
210                    }
211
212                }
213            }
214        }
215    }
216
217    /**
218     * Processes CREATE FUNCTION statements to collect function metadata.
219     * Captures function name, parameters, and return type information.
220     *
221     * @param stmt The CREATE FUNCTION statement to process
222     *
223     * Actions performed:
224     * 1. Creates new function entry in SQL environment
225     * 2. Processes function parameters
226     * 3. Sets function return type
227     */
228    public void preVisit(TCreateFunctionStmt stmt) {
229        if (globalScope.getSqlEnv() == null) return;
230        
231        // Create new function entry
232        TSQLFunction sqlFunction = globalScope.getSqlEnv().addFunction(stmt.getFunctionName().toString(), true);
233        if (sqlFunction == null) return;
234        if (stmt.getParameterDeclarations() == null) return;
235
236        // Process function parameters
237        int c = 0;
238        for(TParameterDeclaration p : stmt.getParameterDeclarations()) {
239            if (p.getParameterName() != null) {
240                sqlFunction.addParameter(c, p.getDataType(), p.getParameterName().toString());
241            } else {
242                sqlFunction.addParameter(c, p.getDataType());
243            }
244        }
245
246        // Set function return type
247        sqlFunction.setReturnType(stmt.getReturnDataType());
248    }
249}