001package gudusoft.gsqlparser.util;
002
003
004import gudusoft.gsqlparser.*;
005import gudusoft.gsqlparser.nodes.*;
006import gudusoft.gsqlparser.resolver2.format.DisplayNameMode;
007import gudusoft.gsqlparser.resolver2.format.DisplayNameNormalizer;
008import gudusoft.gsqlparser.resolver2.format.DisplayNamePolicy;
009import gudusoft.gsqlparser.resolver2.format.IdentityLineSet;
010import gudusoft.gsqlparser.resolver2.format.IdentityNameList;
011import gudusoft.gsqlparser.resolver2.format.StarRedundancyLedger;
012import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
013import gudusoft.gsqlparser.sqlenv.TSQLEnv;
014import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement;
015import gudusoft.gsqlparser.util.joinRelationAnalyze;
016
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Comparator;
021import java.util.Stack;
022
023class myMetaDB implements IMetaDatabase {
024
025    String columns[][] = {
026        {"server","db","DW","AcctInfo_PT","ACCT_ID"},
027        {"server","db","DW","ImSysInfo_BC","ACCT_ID"},
028        {"server","db","DW","AcctInfo_PT","SystemOfRec"},
029        {"server","db","DW","ImSysInfo_BC","SystemOfRec"},
030        {"server","db","DW","AcctInfo_PT","OfficerCode"},
031        {"server","db","DW","ImSysInfo_BC","OpeningDate"},
032    };
033
034    public boolean checkColumn(String server, String database,String schema, String table, String column){
035       boolean bServer,bDatabase,bSchema,bTable,bColumn,bRet = false;
036        for (int i=0; i<columns.length;i++){
037            if ((server == null)||(server.length() == 0)){
038                bServer = true;
039            }else{
040                bServer = columns[i][0].equalsIgnoreCase(server);
041            }
042            if (!bServer) continue;
043
044            if ((database == null)||(database.length() == 0)){
045                bDatabase = true;
046            }else{
047                bDatabase = columns[i][1].equalsIgnoreCase(database);
048            }
049            if (!bDatabase) continue;
050
051            if ((schema == null)||(schema.length() == 0)){
052                bSchema = true;
053            }else{
054                bSchema = columns[i][2].equalsIgnoreCase(schema);
055            }
056
057            if (!bSchema) continue;
058
059            bTable = columns[i][3].equalsIgnoreCase(table);
060            if (!bTable) continue;
061
062            bColumn = columns[i][4].equalsIgnoreCase(column);
063            if (!bColumn) continue;
064
065            bRet =true;
066            break;
067
068        }
069
070        return bRet;
071    }
072
073}
074
075class TInfoRecord {
076
077    public String getSPString(){
078        if (getSPName() == null) return "N/A";
079        else return getSPName().toString();
080    }
081
082    String getTableStr(gudusoft.gsqlparser.nodes.TTable table){
083        String tableName ="";
084        if (table.getTableType() == ETableSource.subquery) {
085            tableName = "(subquery, alias:" + table.getAliasName() + ")";
086        }else{
087            tableName = table.getTableName().toString();
088            if (table.isLinkTable()){
089                tableName = tableName+"("+table.getLinkTable().getTableName().toString()+")";
090            }else if (table.isCTEName()){
091                tableName = tableName+"(CTE)";
092            }
093        }
094
095        return tableName;
096    }
097
098    public String getFullColumnName(){
099        if (dbObjectType != EDbObjectType.column) return "";
100        String columnName = getColumn().getColumnNameOnly();
101
102        if (getTable() != null 
103                        && getTable( ).getObjectNameReferences( ).searchColumnReference( getColumn() )!=-1){
104            if (getTableStr(getTable()).length() > 0){
105                columnName = getTableStr(getTable())+"."+columnName;
106            }
107            schemaName = getTable().getPrefixSchema();
108            if (schemaName.length() > 0) {
109                columnName = schemaName+"."+columnName;
110            }
111            return columnName;
112        }
113        
114       return getColumn().toString( );
115    }
116
117    public String printMe(boolean includingTitle){
118        String spTitle="\nfilename|spname|object type\n";
119        String tableTitle="\nfilename|spname|object type|schema|table|table effect\n";
120        String columnTitle="\nfilename|spname|object type|schema|table|column|location|coordinate|datatype\n";
121        String indexTitle="\nfilename|spname|object type|schema|index|table|column|location|coordinate\n";
122
123        String schemaName = "N/A";
124        String tableName = "unknownTable";
125        String indexName = "unknownIndex";
126
127        StringBuffer  sb = new StringBuffer (1024);
128        switch (dbObjectType){
129            case procedure:
130                if (includingTitle) sb.append(spTitle);
131                sb.append(getFileName()+"|"+getSPName().toString()+"|"+dbObjectType);
132                break;
133            case table:
134                if (includingTitle) sb.append(tableTitle);
135
136                tableName = getTableStr(getTable());
137                schemaName = getTable().getPrefixSchema();
138                if (schemaName.length() == 0) schemaName ="N/A";
139
140                sb.append(getFileName()+"|"+getSPString()+"|"+dbObjectType+"|"+schemaName+"|"+tableName+"|"+getTable().getEffectType());
141                break;
142            case column:
143                if (includingTitle) sb.append(columnTitle);
144                if (getTable() != null){
145                    //it's an orphan column
146                    tableName = getTableStr(getTable());
147                    schemaName = getTable().getPrefixSchema();
148                    if (schemaName.length() == 0) schemaName ="N/A";
149                }else{
150                    tableName = "missed";
151                }
152
153                String datatypeStr = "";
154                if ((getColumn().getLinkedColumnDef() != null)){
155                    //column in create table, add datatype information as well
156                    TTypeName datatype = getColumn().getLinkedColumnDef().getDatatype();
157                    datatypeStr = datatype.getDataTypeName();
158                    if (datatype.getLength() != null){
159                        datatypeStr = datatypeStr+":"+datatype.getLength().toString();
160                    } else if (datatype.getPrecision() != null){
161                        datatypeStr = datatypeStr+":"+datatype.getPrecision().toString();
162                        if (datatype.getScale() != null){
163                            datatypeStr = datatypeStr+":"+datatype.getScale().toString();
164                        }
165                    }
166                }
167                sb.append(getFileName()+"|"+getSPString()+"|"+dbObjectType+"|"+schemaName+"|"+tableName+"|"+getColumn().toString()+"|"+getColumn().getLocation()+"|("+getColumn().coordinate()+")|"+datatypeStr);
168                break;
169            case index:
170                if (includingTitle) sb.append(indexTitle);
171                if (getTable() != null){
172                    schemaName = getTable().getPrefixSchema();
173                    if (schemaName.length() == 0) schemaName ="N/A";
174                    tableName = getTable().getTableName().toString();
175                }
176                if (getIndex() != null){
177                    indexName = getIndex().toString();
178                }
179                sb.append(getFileName()+"|"+getSPString()+"|"+dbObjectType+"|"+schemaName+"|"+indexName+"|"+tableName+"|"+getColumn().toString()+"|"+getColumn().getLocation()+"|("+getColumn().coordinate()+")");
180                break;
181        }
182
183        return sb.toString();
184    }
185
186    private TObjectName index;
187
188    public void setIndex(TObjectName index) {
189        this.index = index;
190    }
191
192    public TObjectName getIndex() {
193
194        return index;
195    }
196
197    private EDbObjectType dbObjectType;
198
199    public void setDbObjectType(EDbObjectType dbObjectType) {
200        this.dbObjectType = dbObjectType;
201    }
202
203    public EDbObjectType getDbObjectType() {
204
205        return dbObjectType;
206    }
207
208    public TInfoRecord(){
209
210    }
211
212    public TInfoRecord(EDbObjectType dbObjectType){
213        this.dbObjectType = dbObjectType;
214    }
215
216    public TInfoRecord(gudusoft.gsqlparser.nodes.TTable  table){
217      this.table = table;
218        this.dbObjectType = EDbObjectType.table;
219    }
220
221    public TInfoRecord(TInfoRecord clone, EDbObjectType dbObjectType){
222        this.fileName = clone.fileName;
223        this.SPName = clone.SPName;
224        this.table = clone.table;
225        this.column = clone.column;
226        this.dbObjectType = dbObjectType;
227    }
228
229    private String fileName = "N/A";
230    private TObjectName SPName ; //stored procedure name
231
232    public void setFileName(String fileName) {
233        this.fileName = fileName;
234    }
235
236    public void setSPName(TObjectName SPName) {
237        this.SPName = SPName;
238    }
239
240    public void setSchemaName(String schemaName) {
241        this.schemaName = schemaName;
242    }
243
244    public String getFileName() {
245
246        return fileName;
247    }
248
249    public TObjectName getSPName() {
250        return SPName;
251    }
252
253    public String getSchemaName() {
254        return schemaName;
255    }
256
257    private String schemaName;
258
259//    public String tableName;
260//    public String columnName;
261
262    private gudusoft.gsqlparser.nodes.TTable  table;
263
264    public void setTable(gudusoft.gsqlparser.nodes.TTable  table) {
265        this.table = table;
266    }
267
268    public void setColumn(TObjectName column) {
269        this.column = column;
270    }
271
272    public gudusoft.gsqlparser.nodes.TTable  getTable() {
273
274        return table;
275    }
276
277    public TObjectName getColumn() {
278        return column;
279    }
280
281    private TObjectName column;
282}
283
284/**
285 * 用于从SQL语句中提取表和列信息的工具类。
286 *
287 * <h2>表输出规则(基于 ETableSource 类型)</h2>
288 *
289 * <h3>默认行为</h3>
290 * <p>默认情况下,此工具<b>仅输出具有表名(getTableName() != null)且不在排除列表中的表</b>。
291 * 以下 {@link ETableSource} 类型的表在默认情况下<b>不会</b>出现在输出的表列表中:</p>
292 * <ul>
293 *   <li>{@link ETableSource#subquery} - 子查询(始终排除)</li>
294 *   <li>{@link ETableSource#openquery} - OPENQUERY 表(始终排除)</li>
295 *   <li>{@link ETableSource#function} - 表值函数(始终排除)</li>
296 *   <li>{@link ETableSource#unnest} - UNNEST 表(因为 getTableName() 为 null,使用别名)</li>
297 *   <li>{@link ETableSource#tableExpr} - 表表达式(因为 getTableName() 为 null,使用别名)</li>
298 *   <li>CTE(公用表表达式)引用的表 - 默认排除,可通过 {@link #showCTE} 控制</li>
299 * </ul>
300 *
301 * <p>以下 {@link ETableSource} 类型的表<b>会</b>出现在默认输出中(如果 getTableName() 不为 null):</p>
302 * <ul>
303 *   <li>{@link ETableSource#objectname} - 普通基础表(始终输出)</li>
304 *   <li>{@link ETableSource#pivoted_table} - PIVOT 表</li>
305 *   <li>{@link ETableSource#lateralView} - LATERAL VIEW 表</li>
306 * </ul>
307 *
308 * <h3>控制选项</h3>
309 *
310 * <h4>1. showCTE 选项</h4>
311 * <p>控制是否输出 CTE(公用表表达式)相关的表:</p>
312 * <ul>
313 *   <li>{@code showCTE = false}(默认):CTE 表不会出现在输出中</li>
314 *   <li>{@code showCTE = true}:CTE 表会出现在输出中</li>
315 * </ul>
316 *
317 * <h4>2. onlyPhysicalTables 选项</h4>
318 * <p>控制是否仅输出物理表(与 {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter} 的 onlyPhysicalTables 选项兼容):</p>
319 * <ul>
320 *   <li>{@code onlyPhysicalTables = false}(默认):输出所有符合条件的表</li>
321 *   <li>{@code onlyPhysicalTables = true}:<b>仅输出 {@link ETableSource#objectname} 类型的物理表</b>,
322 *       排除以下内容:
323 *       <ul>
324 *         <li>{@link ETableSource#unnest} - UNNEST 表</li>
325 *         <li>{@link ETableSource#pivoted_table} - PIVOT 表</li>
326 *         <li>{@link ETableSource#lateralView} - LATERAL VIEW 表</li>
327 *         <li>CTE(公用表表达式)引用的表</li>
328 *       </ul>
329 *   </li>
330 * </ul>
331 *
332 * <h3>如何输出所有类型的表</h3>
333 * <p>要在输出中包含CTE表,请设置 {@link #showCTE} = true:</p>
334 * <pre>{@code
335 * TGetTableColumn getTableColumn = new TGetTableColumn(EDbVendor.dbvoracle);
336 * getTableColumn.showCTE = true;  // 启用CTE表的输出
337 * getTableColumn.runText(sql);
338 * }</pre>
339 *
340 * <p><b>注意:</b>即使设置了 showCTE = true,以下类型的表仍然不会出现在表列表中:</p>
341 * <ul>
342 *   <li>{@link ETableSource#subquery} - 子查询(不是真正的物理表)</li>
343 *   <li>{@link ETableSource#openquery} - OPENQUERY 表</li>
344 *   <li>{@link ETableSource#function} - 表值函数</li>
345 *   <li>{@link ETableSource#unnest} - UNNEST 表(getTableName() 为 null)</li>
346 *   <li>{@link ETableSource#tableExpr} - 表表达式(getTableName() 为 null)</li>
347 * </ul>
348 *
349 * <h3>ETableSource 类型与输出对照表</h3>
350 * <p><b>注意:</b>此表显示的是默认配置下的行为。showUnnest、showPivotTable、showLateralView 默认都为 true,
351 * 所以即使 onlyPhysicalTables=true,这三种表类型默认也会被输出。</p>
352 * <table border="1">
353 *   <tr><th>ETableSource 类型</th><th>默认输出</th><th>showCTE=true</th><th>onlyPhysicalTables=true (默认 show*=true)</th><th>原因</th></tr>
354 *   <tr><td>objectname</td><td>✓ 输出</td><td>✓ 输出</td><td>✓ 输出</td><td>普通基础表</td></tr>
355 *   <tr><td>subquery</td><td>✗ 不输出</td><td>✗ 不输出</td><td>✗ 不输出</td><td>显式排除</td></tr>
356 *   <tr><td>openquery</td><td>✗ 不输出</td><td>✗ 不输出</td><td>✗ 不输出</td><td>显式排除</td></tr>
357 *   <tr><td>function</td><td>✗ 不输出</td><td>✗ 不输出</td><td>✗ 不输出</td><td>显式排除</td></tr>
358 *   <tr><td>CTE 表</td><td>✗ 不输出</td><td>✓ 输出</td><td>✗ 不输出</td><td>由 showCTE 控制,onlyPhysicalTables 排除</td></tr>
359 *   <tr><td>unnest</td><td>✗ 不输出</td><td>✗ 不输出</td><td>✓ 输出</td><td>由 showUnnest 控制(默认 true)</td></tr>
360 *   <tr><td>tableExpr</td><td>✗ 不输出</td><td>✗ 不输出</td><td>✗ 不输出</td><td>getTableName() 为 null</td></tr>
361 *   <tr><td>pivoted_table</td><td>✓ 输出</td><td>✓ 输出</td><td>✓ 输出</td><td>由 showPivotTable 控制(默认 true)</td></tr>
362 *   <tr><td>lateralView</td><td>✓ 输出</td><td>✓ 输出</td><td>✓ 输出</td><td>由 showLateralView 控制(默认 true)</td></tr>
363 * </table>
364 *
365 * <h3>其他相关选项</h3>
366 * <ul>
367 *   <li>{@link #showCTE} - 设置为 true 时,CTE表会包含在输出中</li>
368 *   <li>{@link #onlyPhysicalTables} - 设置为 true 时,仅输出 objectname 类型的物理表</li>
369 *   <li>{@link #showUnnest} - 设置为 true 时(默认),即使 onlyPhysicalTables=true,UNNEST 表也会被输出</li>
370 *   <li>{@link #showPivotTable} - 设置为 true 时(默认),即使 onlyPhysicalTables=true,PIVOT 表也会被输出</li>
371 *   <li>{@link #showLateralView} - 设置为 true 时(默认),即使 onlyPhysicalTables=true,LATERAL VIEW 表也会被输出</li>
372 *   <li>{@link #showColumnsOfCTE} - 设置为 true 时,显示CTE中的列</li>
373 *   <li>{@link #showTableEffect} - 设置为 true 时,显示表的操作类型(SELECT、INSERT、UPDATE、DELETE等)</li>
374 *   <li>{@link #showDetail} - 设置为 true 时,显示详细信息</li>
375 *   <li>{@link #showSummary} - 设置为 true 时(默认),显示汇总信息</li>
376 * </ul>
377 *
378 * <h3>示例</h3>
379 * <pre>{@code
380 * // 示例1:仅获取物理表(默认行为)
381 * TGetTableColumn gtc = new TGetTableColumn(EDbVendor.dbvoracle);
382 * gtc.runText("SELECT * FROM employees e, (SELECT * FROM departments) d");
383 * // 输出仅包含: employees(子查询 departments 不会出现)
384 *
385 * // 示例2:包含CTE表
386 * TGetTableColumn gtc2 = new TGetTableColumn(EDbVendor.dbvoracle);
387 * gtc2.showCTE = true;
388 * gtc2.runText("WITH cte AS (SELECT * FROM t1) SELECT * FROM cte, t2");
389 * // 输出包含: t1, t2, cte(因为 showCTE = true)
390 *
391 * // 示例3:仅输出物理表,但保留 PIVOT 表(默认行为)
392 * TGetTableColumn gtc3 = new TGetTableColumn(EDbVendor.dbvoracle);
393 * gtc3.onlyPhysicalTables = true;
394 * gtc3.runText("SELECT * FROM t1 PIVOT (SUM(amount) FOR month IN ('Jan', 'Feb'))");
395 * // 输出包含: t1 和 PIVOT 表(因为 showPivotTable 默认为 true)
396 *
397 * // 示例4:仅输出物理表,排除 PIVOT 表
398 * TGetTableColumn gtc4 = new TGetTableColumn(EDbVendor.dbvoracle);
399 * gtc4.onlyPhysicalTables = true;
400 * gtc4.showPivotTable = false;  // 排除 PIVOT 表
401 * gtc4.runText("SELECT * FROM t1 PIVOT (SUM(amount) FOR month IN ('Jan', 'Feb'))");
402 * // 输出仅包含: t1(PIVOT 表被排除)
403 * }</pre>
404 *
405 * @see ETableSource
406 * @see ETableSource#objectname
407 */
408public class TGetTableColumn{
409
410
411    private EDbVendor dbVendor;
412    private String queryStr;
413    private TGSqlParser sqlParser;
414    private EResolverType resolverType = EResolverType.RESOLVER; //default to resolver
415
416    public void setResolverType(EResolverType resolverType) {
417        this.resolverType = resolverType;
418    }
419
420    private IMetaDatabase metaDatabase = null;
421
422    public void setMetaDatabase(IMetaDatabase metaDatabase) {
423        this.metaDatabase = metaDatabase;
424        sqlParser.setMetaDatabase(metaDatabase);
425    }
426
427    private TSQLEnv sqlEnv = null;
428
429    public void setSqlEnv(TSQLEnv sqlEnv) {
430        this.sqlEnv = sqlEnv;
431        sqlParser.setSqlEnv(sqlEnv);
432    }
433
434    private StringBuffer functionlist,schemalist,
435    triggerlist,sequencelist,databaselist;
436
437    public StringBuffer infos;
438
439    public StringBuffer outList;
440
441    private ArrayList<TInfoRecord> infoList;
442
443    private ArrayList<String> fieldlist,tablelist,indexList,cteList;
444
445    /**
446     * Star entries held back from {@link #fieldlist} until the list is complete, and the proof
447     * ledger that decides which of them the finished list has made redundant. Kept apart from
448     * the column entries so that pruning can never touch a column, whatever it renders as.
449     */
450    private final java.util.List<String> pendingStarEntries = new java.util.ArrayList<String>();
451    private StarRedundancyLedger starLedger;
452
453    private StringBuffer tableColumnList;
454
455    private String newline  = "\n";
456
457    private String sqlFileName ="N/A";
458
459    public boolean isConsole;
460    /**
461     * Include star columns (*) in output.
462     *
463     * <p>{@code false} mutes a table's own {@code t.*} entry outright. {@code true} does not
464     * force it: the entry then appears only while the star cannot be expanded, i.e. while the
465     * table's columns are not known from metadata. See {@code starEntryShouldBeListed}. That
466     * narrowing needs resolver2; under {@code EResolverType.RESOLVER}/{@code NONE} this flag
467     * alone decides.
468     *
469     * <p>Note the default here is {@code false} while
470     * {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter} defaults its own
471     * {@code listStarColumn} to {@code true}; set it explicitly on both before comparing the
472     * two APIs.
473     */
474    public boolean listStarColumn;
475
476    public boolean showTableEffect;
477    public boolean showColumnLocation;
478    public boolean showDatatype;
479    public boolean showIndex;
480    public boolean showColumnsOfCTE;
481    public boolean showStarColumnOfCTE;
482        public boolean linkOrphanColumnToFirstTable;
483    public boolean showDetail = false;
484    public boolean showSummary = true;
485    public boolean showTreeStructure = false;
486    public boolean showBySQLClause = false;
487    public boolean showJoin = false;
488    public boolean showCTE = false;
489    /**
490     * 控制是否仅输出物理表(与 {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter} 的 onlyPhysicalTables 选项兼容)。
491     * <p>
492     * 当设置为 {@code true} 时,<b>仅输出 {@link ETableSource#objectname} 类型的物理表</b>,
493     * 排除以下类型:
494     * <ul>
495     *   <li>{@link ETableSource#unnest} - UNNEST 表</li>
496     *   <li>{@link ETableSource#pivoted_table} - PIVOT 表</li>
497     *   <li>{@link ETableSource#lateralView} - LATERAL VIEW 表</li>
498     *   <li>CTE(公用表表达式)引用的表</li>
499     * </ul>
500     * </p>
501     * <p>默认值:{@code false}(输出所有符合条件的表,包括 unnest、pivoted_table、lateralView 等)</p>
502     *
503     * @see gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter#setOnlyPhysicalTables(boolean)
504     */
505    public boolean onlyPhysicalTables = false;
506
507    /**
508     * 当设置为 {@code true}(默认)时,即使 {@code onlyPhysicalTables=true},UNNEST 表也会被输出。
509     * 当设置为 {@code false} 时,如果 {@code onlyPhysicalTables=true},UNNEST 表会被排除。
510     *
511     * @see gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter#setShowUnnest(boolean)
512     */
513    public boolean showUnnest = true;
514
515    /**
516     * 当设置为 {@code true}(默认)时,即使 {@code onlyPhysicalTables=true},PIVOT 表也会被输出。
517     * 当设置为 {@code false} 时,如果 {@code onlyPhysicalTables=true},PIVOT 表会被排除。
518     *
519     * @see gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter#setShowPivotTable(boolean)
520     */
521    public boolean showPivotTable = true;
522
523    /**
524     * 当设置为 {@code true}(默认)时,即使 {@code onlyPhysicalTables=true},LATERAL VIEW 表也会被输出。
525     * 当设置为 {@code false} 时,如果 {@code onlyPhysicalTables=true},LATERAL VIEW 表会被排除。
526     *
527     * @see gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter#setShowLateralView(boolean)
528     */
529    public boolean showLateralView = true;
530
531    private Stack<TStoredProcedureSqlStatement> spList;
532
533    /** Display name normalizer for stripping delimiters (same as TSQLResolver2ResultFormatter) */
534    private final DisplayNameNormalizer displayNameNormalizer;
535
536    /** Display name mode (DISPLAY, SQL_RENDER, CANONICAL) - can be changed after instantiation */
537    private DisplayNameMode displayNameMode = DisplayNameMode.SQL_RENDER;
538
539    /** Which spelling to print when one table is written several ways. */
540    private DisplayNamePolicy displayNamePolicy = DisplayNamePolicy.PREFER_DEFINITION_SITE;
541
542    /**
543     * Table names headed for the {@code Tables:} list, de-duplicated by object identity rather
544     * than by rendered text. {@link #tablelist} is filled from it when the summary prints.
545     */
546    private IdentityNameList tableNames;
547
548    /**
549     * What each {@link #fieldlist} line stands for, index for index (null when the line has no
550     * stated object). Kept beside the list so the summary can refuse to fold two different
551     * objects into one line; every write to {@link #fieldlist} goes through {@link #addField}.
552     */
553    private final ArrayList<Object> fieldIdentities = new ArrayList<Object>();
554
555    private void addField(String line, Object identity) {
556        fieldlist.add(line);
557        fieldIdentities.add(identity);
558    }
559
560    class myTokenListHandle implements ITokenListHandle {
561        // 把 ${tx_date_yyyymm} 合并为一个token,token code为 TBasetype.ident
562        public boolean processTokenList(TSourceTokenList sourceTokenList){
563            int startIndex = -1;
564            int endIndex = -1;
565
566            for(int i=0;i< sourceTokenList.size();i++) {
567                TSourceToken token = sourceTokenList.get(i);
568
569                // Check for '$' followed immediately by '{'
570                if (token.tokencode == 36) { // Check for '$'
571                    if (i + 1 < sourceTokenList.size() && sourceTokenList.get(i + 1).tokencode == 123) { // Check for '{' immediately after '$'
572                        startIndex = i;
573                    }
574                } else if (token.tokencode == 125 && startIndex != -1) { // Check for '}'
575                    endIndex = i;
576
577                }
578
579
580                if (startIndex != -1 && endIndex != -1) {
581                    TSourceToken firstToken = sourceTokenList.get(startIndex);
582                    firstToken.tokencode = TBaseType.ident;
583                    for (int j = startIndex + 1; j <= endIndex; j++) {
584                        TSourceToken st = sourceTokenList.get(j);
585                        st.tokenstatus = ETokenStatus.tsdeleted;
586                        firstToken.setString(firstToken.getAstext() + st.getAstext());
587                    }
588
589                    //System.out.println("Found variable token: " + firstToken.toStringDebug());
590
591                    startIndex = -1;
592                    endIndex = -1;
593                }
594            }
595            return true;
596        }
597    }
598
599    String dotChar = ".";
600    public  TGetTableColumn(EDbVendor pDBVendor){
601       dbVendor = pDBVendor;
602
603        sqlParser = new TGSqlParser(dbVendor);
604        // sqlParser.setTokenListHandle(new myTokenListHandle());
605        //sqlParser.setMetaDatabase(new myMetaDB());
606
607        if (TBaseType.isEnableResolver2()) {
608            resolverType = EResolverType.RESOLVER2;
609        } else if (TBaseType.isEnableResolver()) {
610            resolverType = EResolverType.RESOLVER;
611        }
612
613        if (dbVendor == EDbVendor.dbvdax){
614            dotChar = "";
615        }
616        tablelist = new ArrayList<String>();
617        fieldlist = new ArrayList<String>();
618        indexList = new ArrayList<String>();
619        cteList = new ArrayList<String>();
620        infoList = new ArrayList<TInfoRecord>();
621
622        spList = new Stack<TStoredProcedureSqlStatement>();
623
624         infos = new StringBuffer();
625         functionlist = new StringBuffer();
626         schemalist = new StringBuffer();
627         triggerlist = new StringBuffer();
628         sequencelist = new StringBuffer();
629         databaselist  = new StringBuffer();
630        tableColumnList  = new StringBuffer();
631        outList  = new StringBuffer();
632
633        isConsole = true;
634        listStarColumn = false;
635        showTreeStructure = false;
636        showTableEffect = false;
637        showColumnLocation = false;
638        linkOrphanColumnToFirstTable = true;
639        showDatatype = false;
640        showIndex = false;
641        showCTE = false;
642        showColumnsOfCTE = false;
643        showColumnsOfCTE = false;
644
645        // Initialize display name normalizer for consistent identifier formatting
646        displayNameNormalizer = new DisplayNameNormalizer(dbVendor);
647        displayNameNormalizer.setMode(displayNameMode);
648        displayNameNormalizer.setStripDelimiters(true);
649    }
650
651    // ========== Display Name Mode Configuration ==========
652
653    /**
654     * Get the current display name mode.
655     *
656     * @return the current DisplayNameMode
657     */
658    public DisplayNameMode getDisplayNameMode() {
659        return displayNameMode;
660    }
661
662    /**
663     * Set the display name mode for identifier formatting.
664     *
665     * <p>This controls how identifiers (table names, column names) are formatted in output:</p>
666     * <ul>
667     *   <li>{@link DisplayNameMode#DISPLAY} - Strip delimiters, preserve original case
668     *       (e.g., {@code [OrderID]} → {@code OrderID})</li>
669     *   <li>{@link DisplayNameMode#SQL_RENDER} - Preserve delimiters for valid SQL regeneration
670     *       (e.g., {@code [Order ID]} → {@code [Order ID]})</li>
671     *   <li>{@link DisplayNameMode#CANONICAL} - Apply vendor-specific case folding
672     *       (e.g., Oracle: {@code MyTable} → {@code MYTABLE})</li>
673     * </ul>
674     *
675     * <p>This method can be called after instantiation to change the mode for each SQL text
676     * being processed.</p>
677     *
678     * @param mode the DisplayNameMode to use
679     * @return this instance for method chaining
680     */
681    /**
682     * Choose how much delimiting {@link DisplayNameMode#SQL_RENDER} writes. Valid SQL for one
683     * object is a set of spellings ({@code dbo}, {@code [dbo]}), and this picks one:
684     * {@code WHEN_NEEDED} (default) delimits only what must be, {@code ALWAYS} delimits every
685     * segment. Has no effect in the other modes.
686     */
687    public TGetTableColumn setQuotePolicy(gudusoft.gsqlparser.sqlenv.SqlNameGenerator.QuotePolicy policy) {
688        displayNameNormalizer.setQuotePolicy(policy);
689        return this;
690    }
691
692    public gudusoft.gsqlparser.sqlenv.SqlNameGenerator.QuotePolicy getQuotePolicy() {
693        return displayNameNormalizer.getQuotePolicy();
694    }
695
696    public DisplayNamePolicy getDisplayNamePolicy() {
697        return displayNamePolicy;
698    }
699
700    /**
701     * Choose which spelling the {@code Tables:} list prints when the same table is written
702     * several ways (Oracle {@code Sales}, {@code SALES}, {@code "SALES"}). The default,
703     * {@link DisplayNamePolicy#PREFER_DEFINITION_SITE}, falls back to catalog metadata and then
704     * to the first occurrence. A table that is only ever written one way is unaffected.
705     */
706    public TGetTableColumn setDisplayNamePolicy(DisplayNamePolicy policy) {
707        this.displayNamePolicy = policy != null ? policy : DisplayNamePolicy.PREFER_DEFINITION_SITE;
708        return this;
709    }
710
711    public TGetTableColumn setDisplayNameMode(DisplayNameMode mode) {
712        this.displayNameMode = mode != null ? mode : DisplayNameMode.DISPLAY;
713        this.displayNameNormalizer.setMode(this.displayNameMode);
714        return this;
715    }
716
717    public void runText(String pQuery){
718        run(pQuery,false);
719    }
720
721    public void runFile(String pFileName){
722        sqlFileName = pFileName;
723        run(pFileName,true);
724    }
725
726    String numberOfSpace(int pNum){
727        String ret="";
728        for(int i=0;i<pNum;i++){
729            ret = ret+" ";
730        }
731        return ret;
732    }
733
734    public StringBuffer getInfos() {
735        return infos;
736    }
737
738
739    protected void run(String pQuery, boolean isFile){
740        queryStr = pQuery;
741        if (isFile) sqlParser.sqlfilename = pQuery;
742        else sqlParser.sqltext = pQuery;
743        sqlParser.setResolverType(this.resolverType);
744
745        int iRet = sqlParser.parse();
746        if (iRet != 0){
747                if(isConsole)
748                        System.out.println(sqlParser.getErrormessage());
749                else 
750                        throw new RuntimeException(sqlParser.getErrormessage());
751            return;
752        }
753
754        outList.setLength(0);
755        tablelist.clear();
756        tableNames = new IdentityNameList(dbVendor, displayNamePolicy);
757        fieldlist.clear();
758        fieldIdentities.clear();
759        pendingStarEntries.clear();
760        starLedger = null;
761        indexList.clear();
762        cteList.clear();
763
764        for(int i=0;i<sqlParser.sqlstatements.size();i++){
765            analyzeStmt(sqlParser.sqlstatements.get(i),0);
766        }
767
768
769       // print detailed info
770        if (showDetail){
771            boolean includingTitle = true;
772            for(int i=0;i<infoList.size();i++){
773                if (i>0){
774                    includingTitle = !(infoList.get(i).getDbObjectType() == infoList.get(i-1).getDbObjectType());
775                }
776                                outputResult( infoList.get( i ).printMe( includingTitle ) );
777            }
778        }
779
780        pruneFullyExpandedStars();
781
782        // print summary info
783        if (showSummary){
784            // Tables are de-duplicated by identity: comparing rendered text case-insensitively
785            // dropped BigQuery's SALES next to Sales (two tables) and split Oracle's "SALES"
786            // from Sales (one table).
787            tablelist.clear();
788            tablelist.addAll(tableNames.resolve(displayNameMode));
789            // Fields fold as before EXCEPT that lines standing for different objects stay
790            // apart (BigQuery Sales.amount and SALES.amount are two columns).
791            IdentityLineSet fieldLines = new IdentityLineSet();
792            for (int i = 0; i < fieldlist.size(); i++) {
793                fieldLines.add(fieldlist.get(i), i < fieldIdentities.size() ? fieldIdentities.get(i) : null);
794            }
795            fieldlist.clear();
796            fieldIdentities.clear();
797            fieldlist.addAll(fieldLines);
798            removeDuplicateAndSort(indexList);
799            removeDuplicateAndSort(cteList);
800
801            printArray("Tables:", tablelist);
802            outputResult("");
803            printArray("Fields:",fieldlist);
804            if(showIndex && (indexList.size() > 0)){
805                printArray("Indexs:",indexList);
806            }
807
808            if(showColumnsOfCTE && (cteList.size() > 0)){
809                outputResult("");
810                printArray("Ctes:",cteList);
811            }
812        }
813
814        // print tree structure
815        if (showTreeStructure){
816                        outputResult( infos.toString( ) );
817        }
818
819        if (showBySQLClause){
820            ArrayList<ETableEffectType> tableEffectTypes = new ArrayList<ETableEffectType>();
821            ArrayList<ESqlClause> columnClauses = new ArrayList<ESqlClause>();
822
823            for(int i=0;i<infoList.size();i++){
824                if (infoList.get(i).getDbObjectType() == EDbObjectType.table){
825                    if (!tableEffectTypes.contains(infoList.get(i).getTable().getEffectType())){
826                        tableEffectTypes.add(infoList.get(i).getTable().getEffectType());
827                    }
828                }
829            }
830            outputResult("Tables:");
831            for(int j=0;j<tableEffectTypes.size();j++){
832                outputResult("\t"+tableEffectTypes.get(j).toString());
833
834                for(int i=0;i<infoList.size();i++){
835                    if (infoList.get(i).getDbObjectType() == EDbObjectType.table){
836                        gudusoft.gsqlparser.nodes.TTable  lcTable = infoList.get(i).getTable();
837                        if (lcTable.getEffectType() == tableEffectTypes.get(j) && lcTable.getSubquery()==null && lcTable.getTableName()!=null){
838                                outputResult("\t\t" + lcTable.toString()+ "("+lcTable.getTableName().coordinate()+")");
839                        }
840                    }
841                }
842            }
843
844            // column
845            for(int i=0;i<infoList.size();i++){
846                if (infoList.get(i).getDbObjectType() == EDbObjectType.column){
847                    if (!columnClauses.contains(infoList.get(i).getColumn().getLocation())){
848                        columnClauses.add(infoList.get(i).getColumn().getLocation());
849                    }
850                }
851            }
852            outputResult("");
853            outputResult("Columns:");
854            for(int j=0;j<columnClauses.size();j++){
855                outputResult("\t"+columnClauses.get(j).toString());
856
857                for(int i=0;i<infoList.size();i++){
858                    if (infoList.get(i).getDbObjectType() == EDbObjectType.column){
859                        TObjectName lcColumn = infoList.get(i).getColumn();
860                        if (lcColumn.getLocation() == columnClauses.get(j)){
861                                outputResult("\t\t" +  infoList.get(i).getFullColumnName()+ "("+lcColumn.coordinate()+")");
862                        }
863                    }
864                }
865            }
866
867
868        }
869        
870        if(showJoin){
871                joinRelationAnalyze analysis = new joinRelationAnalyze( sqlParser, showColumnLocation );
872                        outputResult( analysis.getAnalysisResult( ) );
873        }
874
875      //  System.out.println("Fields:"+newline+fieldlist.toString());
876    }
877
878
879        private void outputResult( String result)
880        {
881                if (isConsole){
882                     System.out.println(result);
883             //System.out.println(TBaseType.toHex(result,"UTF-8"));
884                 }else {
885                        //if(outList.length()>0)
886                        //      outList.append(newline);
887                     outList.append(result).append( newline);
888                 }
889        }
890
891
892    void printArray(String pTitle,ArrayList<String> pList){
893                outputResult( pTitle );
894                Object str[] = pList.toArray( );
895                for ( int i = 0; i < str.length; i++ )
896                {
897                        outputResult( str[i].toString( ) );
898                }
899    }
900
901
902    void removeDuplicateAndSort(ArrayList <String>  pList){
903        Collections.sort(pList, new SortIgnoreCase() );
904
905        for ( int i = 0 ; i < pList.size() - 1 ; i ++ ) {
906             for ( int j = pList.size() - 1 ; j > i; j -- ) {
907               if (pList.get(j).equalsIgnoreCase((pList.get(i)))) {
908                 pList.remove(j);
909               }
910              }
911            }
912    }
913
914    protected void analyzeStmt(TCustomSqlStatement stmt, int pNest){
915        gudusoft.gsqlparser.nodes.TTable  lcTable = null;
916        TObjectName lcColumn = null;
917        String tn = "",cn="";
918
919        if (stmt instanceof  TStoredProcedureSqlStatement){
920            spList.push((TStoredProcedureSqlStatement)stmt);
921            TInfoRecord spRecord = new TInfoRecord(EDbObjectType.procedure);
922            spRecord.setSPName(spList.peek().getStoredProcedureName());
923        }
924        //System.out.println( numberOfSpace(pNest)+ stmt.sqlstatementtype);
925        infos.append(numberOfSpace(pNest) + stmt.sqlstatementtype+newline);
926
927        for(int i=0;i<stmt.tables.size();i++){
928            //if  (stmt.tables.getTable(i).isBaseTable())
929            //{
930             lcTable = stmt.tables.getTable(i);
931             if (showColumnsOfCTE && lcTable.isCTEName()){
932                 for(TAttributeNode node:lcTable.getAttributes()){
933                     if ((node.getName().endsWith("*")) && (!showStarColumnOfCTE)) continue;
934                     cteList.add(displayNameNormalizer.normalizeQualifiedName(node.getName()));
935                 }
936             }
937             TInfoRecord tableRecord = new TInfoRecord(lcTable);
938            tableRecord.setFileName(this.sqlFileName);
939            if (spList.size() > 0){
940                tableRecord.setSPName(spList.peek().getStoredProcedureName());
941            }
942            infoList.add(tableRecord);
943
944                        if ( lcTable.getTableType( ) == ETableSource.subquery )
945                        {
946                                tn = "(subquery, alias:" + lcTable.getAliasName( ) + ")";
947                        }
948                        else if ( lcTable.getTableType( ) == ETableSource.tableExpr )
949                        {
950                                tn = "(table expression, alias:"
951                                                + lcTable.getAliasName( )
952                                                + ")";
953                        }
954            else if ( lcTable.getTableType( ) == ETableSource.openquery )
955            {
956                tn = "(table openquery, alias:"
957                        + lcTable.getAliasName( )
958                        + ")";
959
960                if (lcTable.getSubquery() != null){
961                    analyzeStmt(lcTable.getSubquery(),pNest++);
962                }
963            }else if (lcTable.getTableType() == ETableSource.function){
964                tn = "(table-valued function:"
965                        + normalizeTableName(lcTable.getTableName())
966                        + ")";
967            }
968            else if (lcTable.getTableType() == ETableSource.pivoted_table){
969                // Check if this is UNPIVOT or PIVOT
970                boolean isUnpivot = false;
971                // First try to get from TPivotClause
972                if (lcTable.getPivotedTable() != null && lcTable.getPivotedTable().getPivotClause() != null) {
973                    isUnpivot = (lcTable.getPivotedTable().getPivotClause().getType() == TPivotClause.unpivot);
974                } else {
975                    // Fallback: check if the table alias suggests UNPIVOT
976                    // When no explicit alias is provided, TPivotClause.doParse() uses "unpivot_alias"
977                    String tableName = lcTable.getTableName() != null ? lcTable.getTableName().toString() : "";
978                    String aliasName = lcTable.getAliasName() != null ? lcTable.getAliasName() : "";
979                    isUnpivot = tableName.startsWith("unpivot_") || aliasName.startsWith("unpivot_");
980                }
981                String prefix = isUnpivot ? "(unpivot-table:" : "(pivot-table:";
982                tn = prefix + normalizeTableName(lcTable.getTableName()) + ")";
983            }
984            else if (lcTable.getTableType() == ETableSource.unnest){
985                tn = "(unnest-table:"
986                        + lcTable.getAliasName( )
987                        + ")";
988            }
989            else if (lcTable.getTableType() == ETableSource.lateralView){
990                tn = "(lateral_view:"
991                        + normalizeTableName(lcTable.getTableName())
992                        + ")";
993            }
994                        else if ( lcTable.getTableName( ) != null )
995                        {
996                                tn = normalizeTableName(lcTable.getTableName());
997                                if ( lcTable.isLinkTable( ) )
998                                {
999                                        tn = tn
1000                                                        + "("
1001                                                        + normalizeTableName(lcTable.getLinkTable().getTableName())
1002                                                        + ")";
1003                                }
1004                                else if ( lcTable.isCTEName( ) )
1005                                {
1006                                        tn = tn + "(CTE)";
1007                                }
1008                        }
1009                //System.out.println(numberOfSpace(pNest+1)+tn.getName());
1010                if ((showTableEffect) &&(lcTable.isBaseTable())){
1011                    infos.append(numberOfSpace(pNest+1)+ tn+"("+lcTable.getEffectType()+")"+newline);
1012                }else{
1013                    infos.append(numberOfSpace(pNest+1)+ tn+newline);
1014                }
1015
1016                tableColumnList.append(","+tn);
1017
1018                // Determine if this table should be added to the tablelist
1019                // When onlyPhysicalTables=true, only ETableSource.objectname tables (non-CTE) are included
1020                // unless showUnnest/showPivotTable/showLateralView are true for those types
1021                boolean shouldAddTable;
1022                if (shouldSkipTableForPhysicalFilter(lcTable)) {
1023                    shouldAddTable = false;
1024                } else {
1025                    shouldAddTable = !((lcTable.getTableType() == ETableSource.subquery)
1026                            || (lcTable.isCTEName()&&(!showCTE))
1027                            ||(lcTable.getTableType() == ETableSource.openquery)
1028                            ||(lcTable.getTableType() == ETableSource.function)) && lcTable.getTableName()!=null;
1029                }
1030
1031                if (shouldAddTable) {
1032                   TObjectName listedName = lcTable.isLinkTable()
1033                           ? lcTable.getLinkTable().getTableName() : lcTable.getTableName();
1034                   String tableName = normalizeTableName(listedName);
1035                   // Add table effect type if showTableEffect is enabled and it's a base table
1036                   String decoration = null;
1037                   if (showTableEffect && lcTable.isBaseTable()) {
1038                       decoration = "(" + lcTable.getEffectType() + ")";
1039                       tableName = tableName + decoration;
1040                   }
1041                   boolean definitionSite = lcTable.getEffectType() == ETableEffectType.tetCreate
1042                           || lcTable.getEffectType() == ETableEffectType.tetCreateAs;
1043                   tableNames.add(tableName, listedName.toString(), ESQLDataObjectType.dotTable,
1044                           decoration, definitionSite, catalogRenderedTableName(listedName, decoration));
1045                }
1046
1047
1048                for (int j=0;j<stmt.tables.getTable(i).getLinkedColumns().size();j++){
1049                    lcColumn = stmt.tables.getTable(i).getLinkedColumns().getObjectName(j);
1050                    if (lcColumn.getValidate_column_status() == TBaseType.MARKED_NOT_A_COLUMN_IN_COLUMN_RESOLVER) continue;
1051                    TInfoRecord columnRecord = new TInfoRecord(tableRecord,EDbObjectType.column);
1052                    columnRecord.setColumn(lcColumn);
1053                    infoList.add(columnRecord);
1054                    cn = normalizeColumnName(lcColumn.getColumnNameOnly());
1055                    if ((showDatatype)&&(lcColumn.getLinkedColumnDef() != null)){
1056                        //column in create table, add datatype information as well
1057                        TTypeName datatype = lcColumn.getLinkedColumnDef().getDatatype();
1058                        cn = cn + ":"+datatype.getDataTypeName();
1059                        if (datatype.getLength() != null){
1060                            cn = cn+":"+datatype.getLength().toString();
1061                        } else if (datatype.getPrecision() != null){
1062                            cn = cn+":"+datatype.getPrecision().toString();
1063                            if (datatype.getScale() != null){
1064                                cn = cn+":"+datatype.getScale().toString();
1065                            }
1066                        } else if (datatype.getDisplayLength() != null){
1067                            cn = cn+":"+datatype.getDisplayLength().toString();
1068                        }
1069                    }
1070                    //System.out.println(numberOfSpace(pNest+2)+cn.getColumnNameOnly());
1071                    if (showColumnLocation){
1072                        String  posStr = "";
1073//                        if ( lcColumn.getColumnToken() != null) {
1074//                            TSourceToken lcStartToken = lcColumn.getColumnToken();
1075//                            posStr ="("+ lcStartToken.lineNo+","+lcStartToken.columnNo+ ")";
1076//                        }
1077                        infos.append(numberOfSpace(pNest+3)+ lcColumn.getColumnNameOnly()+posStr+"("+lcColumn.getLocation()+")"+newline);
1078                    }else{
1079                        infos.append(numberOfSpace(pNest+3)+ lcColumn.getColumnNameOnly()+newline);
1080                    }
1081
1082                    // Determine if this field should be added to the fieldlist
1083                    // When onlyPhysicalTables=true, only include fields from physical tables
1084                    // unless showUnnest/showPivotTable/showLateralView are true for those types
1085                    boolean shouldAddField;
1086                    if (shouldSkipTableForPhysicalFilter(lcTable)) {
1087                        shouldAddField = false;
1088                    } else {
1089                        shouldAddField = !((lcTable.getTableType() == ETableSource.subquery)||(lcTable.isCTEName()&&(!showCTE)));
1090                    }
1091
1092                    if (shouldAddField){
1093                         // Skip star columns for PIVOT tables - individual columns are output via namespace
1094                         // The wildcard is told by its TOKEN, never by its text: a column may
1095                         // legally be named "*" (Oracle, or [*] on SQL Server), and its display
1096                         // name is bare.
1097                         TSourceToken columnPart = lcColumn.getPartToken();
1098                         boolean isStarColumn = columnPart != null && columnPart.tokentype == ETokenType.ttasterisk;
1099                         boolean isPivotTable = lcTable.getTableType() == ETableSource.pivoted_table;
1100                         if (isStarColumn && isPivotTable) {
1101                             // Skip star for pivot tables
1102                         } else if ((listStarColumn) || (!isStarColumn)){
1103                             String fieldName;
1104                             if (lcTable.isLinkTable()){
1105                                 fieldName = normalizeTableName(lcTable.getLinkTable().getTableName()) + dotChar + cn;
1106                             }else{
1107                                 fieldName = tn + dotChar + cn;
1108                             }
1109                             // Add column location if showColumnLocation is enabled
1110                             if (showColumnLocation) {
1111                                 fieldName = fieldName + "(" + lcColumn.getLocation() + ")";
1112                             }
1113                             String fieldPrefix = lcTable.isLinkTable()
1114                                     ? normalizeTableName(lcTable.getLinkTable().getTableName()) : tn;
1115                             if (isStarColumn) {
1116                                 pendingStarEntries.add(fieldName);
1117                                 starLedger().recordStar(fieldName, fieldPrefix, lcTable);
1118                             } else {
1119                                 TObjectName ownerName = lcTable.isLinkTable()
1120                                         ? lcTable.getLinkTable().getTableName() : lcTable.getTableName();
1121                                 addField(fieldName, IdentityLineSet.fieldIdentity(dbVendor,
1122                                         ownerName != null ? ownerName.toString() : null, fieldPrefix,
1123                                         lcColumn.getColumnNameOnly()));
1124                                 starLedger().recordColumn(fieldPrefix, fieldName, lcColumn, lcTable);
1125                             }
1126                         }
1127                    }
1128                    tableColumnList.append(","+tn+dotChar+ cn);
1129                }
1130                //add by grq 2023.07.09 issue=I7ITBQ
1131
1132//                if(stmt.sqlstatementtype.equals(ESqlStatementType.sstinsert)){
1133//                    TInsertSqlStatement insertStmt = (TInsertSqlStatement) stmt;
1134//                    if(insertStmt.getColumnList() == null || insertStmt.getColumnList().size()<=0){
1135//                        TSelectSqlStatement selectStmt = insertStmt.getSubQuery();
1136//                        if(selectStmt != null){
1137//                            for (int j=0;j<selectStmt.getResultColumnList().size();j++){
1138//                                TResultColumn rsColumn = selectStmt.getResultColumnList().getResultColumn(j);
1139//                                if(rsColumn.getLocation().equals(ESqlClause.unknown)){
1140//                                    rsColumn.setLocation(ESqlClause.selectList);
1141//                                }
1142//                                TInfoRecord columnRecord = new TInfoRecord(tableRecord,EDbObjectType.column);
1143//                                TObjectName objectCloumn = rsColumn.getAliasClause()==null? null: rsColumn.getAliasClause().getAliasName();
1144//                                if(objectCloumn == null){
1145//                                    objectCloumn = rsColumn.getColumnFullname();
1146//                                }
1147//                                columnRecord.setColumn(objectCloumn);
1148//                                infoList.add(columnRecord);
1149//                                cn = rsColumn.getColumnAlias();
1150//                                if(StringUtils.isEmpty(cn)){
1151//                                    cn = rsColumn.getColumnNameOnly();
1152//                                }
1153//                                if (showColumnLocation){
1154//                                    String  posStr = "";
1155//                                    infos.append(numberOfSpace(pNest+3)+ cn + posStr+"("+rsColumn.getLocation()+")"+newline);
1156//                                }else{
1157//                                    infos.append(numberOfSpace(pNest+3)+ cn + newline);
1158//                                }
1159//
1160//                                if (!((lcTable.getTableType() == ETableSource.subquery)||(lcTable.isCTEName()&&(!showCTE)))){
1161//                                    if ((listStarColumn) || (!(rsColumn.getColumnNameOnly().equals("*")))){
1162//                                        if (lcTable.isLinkTable()){
1163//                                            fieldlist.add(lcTable.getLinkTable().getTableName() + dotChar + cn );
1164//                                        }else{
1165//                                            fieldlist.add(tn + dotChar + cn );
1166//                                        }
1167//                                    }
1168//                                }
1169//                                tableColumnList.append(","+tn+dotChar+ cn);
1170//                            }
1171//                        }
1172//                    }
1173//                }
1174                //end by grq
1175            //}
1176        }
1177
1178        if (stmt.getOrphanColumns().size() > 0){
1179           // infos.append(numberOfSpace(pNest+1)+" orphan columns:"+newline);
1180            String oc = "";
1181            for (int k=0;k<stmt.getOrphanColumns().size();k++){
1182                TObjectName orphanCol = stmt.getOrphanColumns().getObjectName(k);
1183                // Skip if already resolved
1184                if (orphanCol.getResolveStatus() == TBaseType.RESOLVED_AND_FOUND)
1185                    continue;
1186                // Skip if column already has a sourceTable (it's been resolved to a table)
1187                // This handles cases like UNNEST where the table is virtual, not physical
1188                if (orphanCol.getSourceTable() != null)
1189                    continue;
1190                TInfoRecord columnRecord = new TInfoRecord(EDbObjectType.column);
1191                columnRecord.setColumn(stmt.getOrphanColumns().getObjectName(k));
1192                columnRecord.setFileName(this.sqlFileName);
1193                infoList.add(columnRecord);
1194
1195                oc = normalizeColumnName(stmt.getOrphanColumns().getObjectName(k).getColumnNameOnly());// stmt.getOrphanColumns().getObjectName(k).toString();
1196                if (showColumnLocation){
1197                    infos.append(numberOfSpace(pNest+3)+oc+"("+stmt.getOrphanColumns().getObjectName(k).getLocation()+")"+newline);
1198                }else{
1199                    infos.append(numberOfSpace(pNest+3)+oc+newline);
1200                }
1201
1202                if ((linkOrphanColumnToFirstTable)&&(stmt.getFirstPhysicalTable() != null)){
1203                    if ((listStarColumn) ||(!(oc.equalsIgnoreCase("*")))) {
1204                        String fieldName = normalizeTableName(stmt.getFirstPhysicalTable().getTableName()) + dotChar + oc;
1205                        // Add column location if showColumnLocation is enabled
1206                        if (showColumnLocation) {
1207                            fieldName = fieldName + "(" + stmt.getOrphanColumns().getObjectName(k).getLocation() + ")";
1208                        }
1209                        addOutsideLedger(fieldName);
1210                    }
1211                    columnRecord.setTable(stmt.getFirstPhysicalTable());
1212                }else {
1213                    String fieldName = "missed" + dotChar + oc + "(" + stmt.getOrphanColumns().getObjectName(k).coordinate() + ")";
1214                    // Add column location if showColumnLocation is enabled (for orphan columns)
1215                    if (showColumnLocation) {
1216                        fieldName = fieldName + "(" + stmt.getOrphanColumns().getObjectName(k).getLocation() + ")";
1217                    }
1218                    addOutsideLedger(fieldName);
1219                }
1220                tableColumnList.append(",missed"+dotChar+oc+newline);
1221
1222                TObjectName column = stmt.getOrphanColumns().getObjectName(k);
1223//                if (column.isOrphanColumn()){
1224//                    System.out.println("Candidate table size of column: "+ column.toString()+" ,"+ column.getCandidateTables().size());
1225//                }
1226
1227            }
1228        }
1229
1230        for(int i=0;i<stmt.getIndexColumns().size();i++){
1231            TColumnWithSortOrder indexColumn = stmt.getIndexColumns().getElement(i);
1232            TInfoRecord indexRecord = new TInfoRecord(EDbObjectType.index);
1233            indexRecord.setColumn(indexColumn.getColumnName());
1234            indexRecord.setFileName(this.sqlFileName);
1235            infoList.add(indexRecord);
1236
1237
1238            String tableName = "unknownTable";
1239            String indexName = "unknownIndex";
1240            if (indexColumn.getOwnerTable() != null){
1241                tableName = normalizeTableName(indexColumn.getOwnerTable().getTableName());
1242                indexRecord.setTable(indexColumn.getOwnerTable());
1243            }
1244            if (indexColumn.getOwnerConstraint() != null){
1245                if (indexColumn.getOwnerConstraint().getConstraintName() != null){
1246                    indexName = normalizeColumnName(indexColumn.getOwnerConstraint().getConstraintName().toString());
1247                    indexRecord.setIndex(indexColumn.getOwnerConstraint().getConstraintName());
1248                }
1249            }
1250            indexList.add(tableName + ":" + normalizeColumnName(indexColumn.getColumnName().toString())+":"+indexName);
1251        }
1252
1253        for (int i=0;i<stmt.getStatements().size();i++){
1254            analyzeStmt(stmt.getStatements().get(i),pNest+1);
1255        }
1256
1257        if (stmt instanceof  TStoredProcedureSqlStatement){
1258            TStoredProcedureSqlStatement p = (TStoredProcedureSqlStatement)stmt;
1259            for(int i=0;i<p.getBodyStatements().size();i++){
1260                analyzeStmt(p.getBodyStatements().get(i),pNest+1);
1261            }
1262            spList.pop();
1263        }
1264
1265    }
1266
1267    /**
1268     * 判断表是否为物理表(基于 {@link ETableSource#objectname} 类型)。
1269     * <p>
1270     * 仅当表类型为 {@link ETableSource#objectname} 且不是 CTE 引用时,返回 true。
1271     * 这与 {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter#isPhysicalTable} 的行为一致。
1272     * </p>
1273     *
1274     * @param table 要检查的表
1275     * @return 如果是物理表返回 true,否则返回 false
1276     */
1277    private boolean isPhysicalTable(gudusoft.gsqlparser.nodes.TTable table) {
1278        if (table == null) return false;
1279
1280        // Only objectname type tables can be physical tables
1281        // This filters out subqueries, joins, functions, pivoted tables, unnest, lateralView, etc.
1282        if (table.getTableType() != ETableSource.objectname) {
1283            return false;
1284        }
1285
1286        // Tables from CTE are considered virtual, not physical
1287        if (table.isCTEName()) {
1288            return false;
1289        }
1290
1291        return true;
1292    }
1293
1294    /**
1295     * 检查表是否应该被跳过,基于 onlyPhysicalTables 和 show* 选项。
1296     *
1297     * <p>当 onlyPhysicalTables=true 时,表会被跳过,除非:</p>
1298     * <ul>
1299     *   <li>它是物理表(isPhysicalTable 返回 true)且不是 CTAS 目标表</li>
1300     *   <li>它是 UNNEST 表且 showUnnest=true</li>
1301     *   <li>它是 PIVOT 表且 showPivotTable=true</li>
1302     *   <li>它是 LATERAL VIEW 表且 showLateralView=true</li>
1303     * </ul>
1304     *
1305     * @param table 要检查的表
1306     * @return 如果表应该被跳过返回 true,如果应该包含返回 false
1307     */
1308    private boolean shouldSkipTableForPhysicalFilter(gudusoft.gsqlparser.nodes.TTable table) {
1309        if (!onlyPhysicalTables) {
1310            return false; // Not filtering, don't skip
1311        }
1312
1313        // CTAS target tables are DDL targets, not existing physical tables
1314        // They have ETableEffectType.tetCreateAs effect type
1315        if (table.getEffectType() == ETableEffectType.tetCreateAs) {
1316            return true;
1317        }
1318
1319        // Physical tables are always included when onlyPhysicalTables=true
1320        if (isPhysicalTable(table)) {
1321            return false;
1322        }
1323
1324        // Check if table type should be included based on show* options
1325        ETableSource tableType = table.getTableType();
1326        if (tableType == ETableSource.unnest && showUnnest) {
1327            return false;
1328        }
1329        if (tableType == ETableSource.pivoted_table && showPivotTable) {
1330            return false;
1331        }
1332        if (tableType == ETableSource.lateralView && showLateralView) {
1333            return false;
1334        }
1335
1336        // All other non-physical tables are skipped when onlyPhysicalTables=true
1337        return true;
1338    }
1339
1340    // ========== Identifier Normalization Methods ==========
1341
1342    /**
1343     * Normalize a table name for consistent output formatting.
1344     *
1345     * <p>This uses the same normalization mechanism as
1346     * {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter} to ensure
1347     * uniform identifier formatting across both tools.</p>
1348     *
1349     * <p>The normalization strips delimiters (quotes, brackets, backticks) while
1350     * preserving the original case. This handles:</p>
1351     * <ul>
1352     *   <li>Double quotes: {@code "table_name"} → {@code table_name}</li>
1353     *   <li>Square brackets: {@code [table_name]} → {@code table_name}</li>
1354     *   <li>Backticks: {@code `table_name`} → {@code table_name}</li>
1355     *   <li>Multi-part names: {@code [schema].[table]} → {@code schema.table}</li>
1356     * </ul>
1357     *
1358     * @param tableName The TObjectName representing the table name
1359     * @return The normalized table name string
1360     */
1361    /** The catalog rung of the display-name policy; see {@link IdentityNameList#renderCatalogTableName}. */
1362    private String catalogRenderedTableName(TObjectName tableName, String decoration) {
1363        String rendered = IdentityNameList.renderCatalogTableName(dbVendor, sqlEnv, tableName, displayNameMode,
1364                displayNameNormalizer.getQuotePolicy());
1365        if (rendered == null) return null;
1366        return decoration == null ? rendered : rendered + decoration;
1367    }
1368
1369    private String normalizeTableName(TObjectName tableName) {
1370        if (tableName == null) return "";
1371        String fullName = tableName.toString();
1372        if (fullName == null || fullName.isEmpty()) return "";
1373        return displayNameNormalizer.normalizeQualifiedName(fullName, ESQLDataObjectType.dotTable);
1374    }
1375
1376    /**
1377     * Normalize a column name for consistent output formatting.
1378     *
1379     * <p>This uses the same normalization mechanism as
1380     * {@link gudusoft.gsqlparser.resolver2.TSQLResolver2ResultFormatter} to ensure
1381     * uniform identifier formatting across both tools.</p>
1382     *
1383     * @param columnName The raw column name which may include quotes
1384     * @return The normalized column name
1385     */
1386    private String normalizeColumnName(String columnName) {
1387        if (columnName == null || columnName.isEmpty()) return columnName;
1388        return displayNameNormalizer.normalizeIdentifier(columnName);
1389    }
1390
1391    /**
1392     * Release the held-back star entries into {@link #fieldlist}, minus the ones the finished
1393     * list has made redundant.
1394     *
1395     * <p>A {@code t.*} entry is dropped only once every one of that table's known columns
1396     * reached the list under the same prefix, proven by identity of what was emitted — see
1397     * {@link StarRedundancyLedger}. Metadata alone is not enough: a table with a column the
1398     * query never names keeps its star, since the star is then the only remaining record that
1399     * the table has more columns. Expandability can only be judged from resolver2's scope
1400     * build; without it ({@code EResolverType.RESOLVER} / {@code NONE}) nothing is pruned and
1401     * {@link #listStarColumn} alone decides, as before.
1402     */
1403    private void pruneFullyExpandedStars() {
1404        if (!pendingStarEntries.isEmpty()) {
1405            java.util.Set<String> redundant = starLedger().redundantStarEntries(fieldlist);
1406            for (String starEntry : pendingStarEntries) {
1407                if (!redundant.contains(starEntry)) addField(starEntry, null);
1408            }
1409            pendingStarEntries.clear();
1410        }
1411        if (starLedger != null) starLedger.clear();
1412    }
1413
1414    /**
1415     * Put a line into {@link #fieldlist} that no ledger record accounts for (an orphan linked to
1416     * the first table, a {@code missed.} line), and tell the ledger so: the line still folds
1417     * with recorded lines in the list's de-duplication, and a member whose only line it
1418     * swallowed has not reached the reader.
1419     */
1420    private void addOutsideLedger(String line) {
1421        addField(line, null);
1422        starLedger().recordForeignLine(line);
1423    }
1424
1425    private StarRedundancyLedger starLedger() {
1426        if (starLedger == null) {
1427            starLedger = new StarRedundancyLedger(
1428                    sqlParser == null ? null : sqlParser.getResolver2(), dbVendor);
1429        }
1430        return starLedger;
1431    }
1432
1433}
1434
1435 class SortIgnoreCase implements Comparator<Object> {
1436        public int compare(Object o1, Object o2) {
1437            String s1 = (String) o1;
1438            String s2 = (String) o2;
1439            return s1.toLowerCase(java.util.Locale.ROOT).compareTo(s2.toLowerCase(java.util.Locale.ROOT));
1440        }
1441    }
1442
1443