001
002package gudusoft.gsqlparser.dlineage.dataflow.model;
003
004import java.util.ArrayList;
005import java.util.LinkedHashMap;
006import java.util.LinkedHashSet;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010
011import gudusoft.gsqlparser.EDbVendor;
012import gudusoft.gsqlparser.EExpressionType;
013import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
014import gudusoft.gsqlparser.TSourceToken;
015import gudusoft.gsqlparser.dlineage.util.DlineageUtil;
016import gudusoft.gsqlparser.dlineage.util.Pair3;
017import gudusoft.gsqlparser.nodes.TConstant;
018import gudusoft.gsqlparser.nodes.TExpression;
019import gudusoft.gsqlparser.nodes.TFunctionCall;
020import gudusoft.gsqlparser.nodes.TObjectName;
021import gudusoft.gsqlparser.nodes.TResultColumn;
022import gudusoft.gsqlparser.nodes.TResultColumnList;
023import gudusoft.gsqlparser.sqlenv.TSQLEnv;
024import gudusoft.gsqlparser.util.SQLUtil;
025
026public class TableColumn extends Column{
027
028
029    private Table table;
030
031    private long id;
032    protected String name;
033
034    protected Pair3<Long, Long, String> startPosition;
035    protected Pair3<Long, Long, String> endPosition;
036
037    private TObjectName columnObject;
038    private Map<String, Set<TObjectName>> starLinkColumns = new LinkedHashMap<String, Set<TObjectName>>();
039    
040    private boolean showStar;
041    
042    private boolean expandStar = true;
043    
044        private Integer columnIndex;
045        
046        private boolean isVariant;
047        
048        private boolean isPseduo = false;
049        
050        private boolean notBindStarColumn = false;
051        
052        private String displayName;
053
054    private String dataType;
055
056    private Boolean primaryKey = false;
057
058    private Boolean unqiueKey = false;
059
060    private Boolean indexKey = false;
061
062    private Boolean foreignKey = false;
063    
064    private boolean isStruct = false;
065
066        private List<Transform> transforms;
067        
068    private List<Object> candidateParents;
069    
070    private TableColumn sourceColumn;
071
072        public TableColumn(Table view, TObjectName columnObject, int index) {
073                this(view, columnObject);
074                this.columnIndex = index;
075        }
076        
077    public TableColumn(Table table, TObjectName columnObject) {
078        if (table == null || columnObject == null)
079            throw new IllegalArgumentException("TableColumn arguments can't be null.");
080
081        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
082
083        this.columnObject = columnObject;
084
085        TSourceToken startToken = columnObject.getStartToken();
086        TSourceToken endToken = columnObject.getEndToken();
087        this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo,
088                startToken.columnNo, ModelBindingManager.getGlobalHash());
089        this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
090                endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
091
092        if (columnObject.getColumnNameOnly() != null
093                && !"".equals(columnObject.getColumnNameOnly())) {
094                        if (columnObject.getPartToken() != null && columnObject.getPropertyToken() != null) {
095                                if(supportProperty() && columnObject.getSourceTable() == table.getTableObject()) {
096                                        // Use resolver info to build struct name without table alias prefix
097                                        String structName = null;
098                                        gudusoft.gsqlparser.resolver2.model.ColumnSource cs = columnObject.getColumnSource();
099                                        if (cs != null && cs.isStructFieldAccess() && cs.hasFieldPath()) {
100                                                structName = cs.getFieldPath().toFullReference(cs.getExposedName());
101                                        }
102                                        if (structName == null) {
103                                                gudusoft.gsqlparser.resolver2.model.StructFieldHint hint = columnObject.getStructFieldHint();
104                                                if (hint != null && hint.getFieldPath() != null) {
105                                                        structName = hint.toFullReference();
106                                                }
107                                        }
108                                        if (structName != null) {
109                                                this.name = structName;
110                                        } else {
111                                                // Non-struct qualified name: keep original toString() behavior
112                                                this.name = columnObject.toString();
113                                        }
114                                }
115                                else {
116                                        this.name = DlineageUtil.decodeColumnIdentifier(columnObject.getPartToken().getAstext())
117                                                        + "." + DlineageUtil.decodeColumnIdentifier(columnObject.getPropertyToken().getAstext());
118                                }
119                        }
120                        else if(columnObject.getColumnSource()!=null && columnObject.getColumnSource().isStructFieldAccess()) {
121                                if(supportProperty() && columnObject.getSourceTable() == table.getTableObject()) {
122                                        // Use resolver info to build struct name without table alias prefix
123                                        String structName = null;
124                                        gudusoft.gsqlparser.resolver2.model.ColumnSource cs = columnObject.getColumnSource();
125                                        if (cs != null && cs.hasFieldPath()) {
126                                                structName = cs.getFieldPath().toFullReference(cs.getExposedName());
127                                        }
128                                        if (structName != null) {
129                                                this.name = structName;
130                                        } else {
131                                                this.name = columnObject.toString();
132                                        }
133                                } else {
134                                        this.name = DlineageUtil.decodeColumnIdentifier(columnObject.getColumnNameOnly());
135                                }
136                        }
137                        else if(columnObject.getStructFieldHint() != null && supportProperty()
138                                        && !isQualifiedColumnReference(columnObject)) {
139                                this.name = columnObject.getStructFieldHint().toFullReference();
140                        }
141                        else {
142                                this.name = DlineageUtil.decodeColumnIdentifier(columnObject.getColumnNameOnly());
143                        }
144        }
145        else if (columnObject.toString().indexOf(".") != -1 && "".equals(columnObject.getColumnNameOnly()) &&
146                        supportProperty()) {
147                this.name = columnObject.toString();
148                }
149                else if (columnObject.toString().indexOf(".") != -1) {
150                        List<String> splits = SQLUtil.parseNames(columnObject.toString());
151                        this.name = DlineageUtil.decodeColumnIdentifier(splits.get(splits.size() - 1));
152                }
153        else {
154            // A SELECT alias reaches here with a null part token
155            // (dbObjectType=column_alias, so getColumnNameOnly() is empty), which
156            // is why a CREATE VIEW published '[Volgorde]' / 'Volgorde' verbatim
157            // as the view's column name (GitHub issue #704). The argument is a
158            // TObjectName — an identifier by construction, never a value
159            // literal — so decoding its delimiters here is sound.
160            this.name = DlineageUtil.decodeAliasIdentifier(columnObject.toString());
161        }
162
163        this.table = table;
164        table.addColumn(this);
165        
166        if("*".equals(this.name) && !table.isDetermined()){
167                showStar = true;
168        }
169    }
170
171        protected boolean supportProperty() {
172                return ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery || ModelBindingManager.getGlobalVendor() == EDbVendor.dbvredshift;
173        }
174
175        /**
176         * Check if a column reference is a standard qualified table.column reference
177         * (not a struct field access). Detects when tableToken matches the source table's name,
178         * e.g., "catalog.Hospital.table_update_01.Position" where table_update_01 is the table.
179         */
180        private boolean isQualifiedColumnReference(TObjectName columnObject) {
181                if (columnObject.getSourceTable() == null || columnObject.getTableToken() == null) {
182                        return false;
183                }
184                String tableTokenStr = columnObject.getTableToken().getAstext();
185                gudusoft.gsqlparser.nodes.TTable src = columnObject.getSourceTable();
186                // Compare tableToken with the source table's simple name (last segment)
187                String tableName = src.getTableName() != null ? src.getTableName().getTableString() : "";
188                List<String> parts = SQLUtil.parseNames(tableName);
189                String simpleTableName = parts.isEmpty() ? tableName : parts.get(parts.size() - 1);
190                if (sameTableName(tableTokenStr, simpleTableName)) {
191                        return true;
192                }
193                // Also check alias
194                String aliasName = src.getAliasName();
195                if (aliasName != null && sameTableName(tableTokenStr, aliasName)) {
196                        return true;
197                }
198                return false;
199        }
200
201        /**
202         * Table-name/alias equality routed through the unified façade; falls back to
203         * case-insensitive comparison when no analysis vendor is bound.
204         */
205        private static boolean sameTableName(String a, String b) {
206                if (a == null || b == null) {
207                        return false;
208                }
209                EDbVendor vendor = ModelBindingManager.getGlobalVendor();
210                return vendor == null ? a.equalsIgnoreCase(b)
211                                : SQLUtil.sameName(vendor, ESQLDataObjectType.dotTable, a, b);
212        }
213
214    /**
215     * Constructor for MDX and other non-SQL columns where only a name is available.
216     */
217    public TableColumn(Table table, String columnName) {
218        if (table == null || columnName == null)
219            throw new IllegalArgumentException("TableColumn arguments can't be null.");
220
221        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
222
223        this.startPosition = new Pair3<Long, Long, String>(-1L, -1L, ModelBindingManager.getGlobalHash());
224        this.endPosition = new Pair3<Long, Long, String>(-1L, -1L, ModelBindingManager.getGlobalHash());
225
226        this.name = columnName;
227        this.table = table;
228        table.addColumn(this);
229    }
230
231    public TableColumn(Table table, TConstant columnObject, int columnIndex) {
232        if (table == null || columnObject == null)
233            throw new IllegalArgumentException("TableColumn arguments can't be null.");
234
235        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
236
237        TSourceToken startToken = columnObject.getStartToken();
238        TSourceToken endToken = columnObject.getEndToken();
239        this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo,
240                startToken.columnNo, ModelBindingManager.getGlobalHash());
241        this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
242                endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
243
244        this.name = columnObject.toString();
245        
246        this.table = table;
247        table.addColumn(this);
248    }
249    
250    public TableColumn(Table table, TConstant columnObject) {
251        if (table == null || columnObject == null)
252            throw new IllegalArgumentException("TableColumn arguments can't be null.");
253
254        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
255
256        TSourceToken startToken = columnObject.getStartToken();
257        TSourceToken endToken = columnObject.getEndToken();
258        this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo,
259                startToken.columnNo, ModelBindingManager.getGlobalHash());
260        this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
261                endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
262
263        this.name = columnObject.toString();        
264        this.table = table;
265        table.addColumn(this);
266    }
267    
268    public TableColumn(Table table, TFunctionCall columnObject) {
269        if (table == null || columnObject == null)
270            throw new IllegalArgumentException("TableColumn arguments can't be null.");
271
272        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
273
274        TSourceToken startToken = columnObject.getStartToken();
275        TSourceToken endToken = columnObject.getEndToken();
276        this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo,
277                startToken.columnNo, ModelBindingManager.getGlobalHash());
278        this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
279                endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
280
281        this.name = columnObject.toString();        
282        this.table = table;
283        table.addColumn(this);
284        }
285
286    
287    public TableColumn(Table table, TExpression columnObject, int columnIndex) {
288        if (table == null || columnObject == null)
289            throw new IllegalArgumentException("TableColumn arguments can't be null.");
290
291        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
292
293        TSourceToken startToken = columnObject.getStartToken();
294        TSourceToken endToken = columnObject.getEndToken();
295        this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo,
296                startToken.columnNo, ModelBindingManager.getGlobalHash());
297        this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
298                endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
299
300                this.name = getColumnName(columnObject);
301        
302        this.table = table;
303        table.addColumn(this);
304    }
305    
306    protected String getColumnName(TExpression expr) {
307                if (expr.getExpressionType() == EExpressionType.simple_constant_t) {
308                        return expr.toString();
309                } else if (expr.getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) {
310                        // "alias = expression": no TAliasClause exists, but the left operand
311                        // is an alias by grammar, so it decodes like one (issue #704).
312                        return DlineageUtil.decodeAliasIdentifier(expr.getLeftOperand().toString());
313                } else if (expr.getExpressionType() == EExpressionType.function_t) {
314                        return expr.getFunctionCall().toString();
315                } else if (expr.getExpressionType() == EExpressionType.typecast_t) {
316                        return expr.getLeftOperand().toString();
317                } else if (expr.getExpressionType() == EExpressionType.parenthesis_t) {
318                        TExpression inner = expr.getLeftOperand();
319                        while (inner != null && inner.getExpressionType() == EExpressionType.parenthesis_t) {
320                                inner = inner.getLeftOperand();
321                        }
322                        return getColumnName(inner);
323                } else if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
324                        TObjectName columnName = expr.getObjectOperand();
325                        if (columnName.getPropertyToken() != null) {
326                                return DlineageUtil.decodeColumnIdentifier(columnName.getPropertyToken().getAstext());
327                        } else {
328                                return DlineageUtil.decodeColumnIdentifier(columnName.getColumnNameOnly());
329                        }
330                } else if (expr.getExpressionType() == EExpressionType.subquery_t) {
331                        TResultColumnList resultset = expr.getSubQuery().getResultColumnList();
332                        if (resultset != null && resultset.size() == 1) {
333                                TResultColumn resultColumn = resultset.getResultColumn(0);
334                                if (!SQLUtil.isEmpty(resultColumn.getColumnAlias())) {
335                                        return resultColumn.getColumnAlias();
336                                } else if (resultColumn.getExpr() != null) {
337                                        return getColumnName(resultColumn.getExpr());
338                                } else if (resultColumn.getColumnNameOnly() != null
339                                                && !"".equals(resultColumn.getColumnNameOnly())) {
340                                        if (resultColumn.getExpr().getObjectOperand() != null
341                                                        && resultColumn.getExpr().getObjectOperand().getPropertyToken() != null) {
342                                                this.name = DlineageUtil.decodeColumnIdentifier(resultColumn.getExpr().getObjectOperand().getPropertyToken().getAstext());
343                                        } else {
344                                                this.name = DlineageUtil.decodeColumnIdentifier(resultColumn.getColumnNameOnly());
345                                        }
346                                } else {
347                                        this.name = resultColumn.toString();
348                                }
349                        }
350                }
351                return quote(expr.toString());
352        }
353    
354 
355        private String quote(String column) {
356        EDbVendor vendor = ModelBindingManager.getGlobalVendor();
357        if (!DlineageUtil.isQuote(column)){
358                return SQLUtil.quoteDottedName(vendor, ESQLDataObjectType.dotColumn, column);
359        }
360        else return column;
361    }
362
363    public Table getTable() {
364        return table;
365    }
366
367    public long getId() {
368        return id;
369    }
370
371    public String getName() {
372        return name;
373    }
374
375    public Pair3<Long, Long, String> getStartPosition() {
376        return startPosition;
377    }
378
379    public Pair3<Long, Long, String> getEndPosition() {
380        return endPosition;
381    }
382
383    public void setStartPosition(Pair3<Long, Long, String> startPosition) {
384        this.startPosition = startPosition;
385    }
386
387    public void setEndPosition(Pair3<Long, Long, String> endPosition) {
388        this.endPosition = endPosition;
389    }
390
391    public TObjectName getColumnObject() {
392        return columnObject;
393    }
394
395    public void bindStarLinkColumns(Map<String, Set<TObjectName>> starLinkColumns) {
396        if (starLinkColumns != null && !starLinkColumns.isEmpty()) {
397            this.starLinkColumns.putAll(starLinkColumns);
398        }
399    }
400
401    public Map<String, Set<TObjectName>> getStarLinkColumns() {
402        return starLinkColumns;
403    }
404    
405        public boolean hasStarLinkColumn(){
406                return (starLinkColumns!=null && !starLinkColumns.isEmpty());
407        }
408    
409        public boolean bindStarLinkColumn(TObjectName objectName) {
410                if (objectName == null) {
411                        return false;
412                }
413
414                String columnName = DlineageUtil.getColumnName(objectName);
415
416                if ("*".equals(columnName)) {
417                        return false;
418                }
419                
420                boolean flag = false;
421                if (!starLinkColumns.containsKey(columnName)) {
422                        starLinkColumns.put(columnName, new LinkedHashSet<TObjectName>());
423                        flag = true;
424                }
425
426                starLinkColumns.get(columnName).add(objectName);
427                
428                
429                if(this.getStarSourceColumns()!=null) {
430                        for(Column column: this.getStarSourceColumns()) {
431                                boolean bind = true;
432                                if(column instanceof ResultColumn) {
433                                        ResultSet parent = ((ResultColumn) column).getResultSet();
434                                        for(ResultColumn item: parent.getColumns()) {
435                                                if(DlineageUtil.compareColumnIdentifier(DlineageUtil.getColumnName(item.getName()), DlineageUtil.getColumnName(objectName))) {
436                                                        bind = false;
437                                                        break;
438                                                }
439                                        }
440                                }
441                                if(column instanceof TableColumn) {
442                                        Table parent = ((TableColumn) column).getTable();
443                                        for(TableColumn item: parent.getColumns()) {
444                                                if(DlineageUtil.compareColumnIdentifier(DlineageUtil.getColumnName(item.getName()), DlineageUtil.getColumnName(objectName))) {
445                                                        bind = false;
446                                                        break;
447                                                }
448                                        }
449                                }
450                                if (bind) {
451                                        column.bindStarLinkColumn(objectName);
452                                }
453                        }
454                }
455                
456                return flag;
457        }
458    
459        public List<TObjectName> getStarLinkColumnList() {
460                List<TObjectName> columns = new ArrayList<TObjectName>();
461                if (starLinkColumns != null) {
462                        for (Set<TObjectName> columnSet : starLinkColumns.values()) {
463                                columns.addAll(columnSet);
464                        }
465                }
466                return columns;
467        }
468        
469        public List<String> getStarLinkColumnNames() {
470                List<String> columns = new ArrayList<>();
471                if (starLinkColumns != null) {
472                        columns.addAll(starLinkColumns.keySet());
473                }
474                return columns;
475        }
476
477        public boolean isShowStar() {
478                return showStar;
479        }
480
481        public void setShowStar(boolean showStar) {
482                this.showStar = showStar;
483        }
484        
485        public Table getView() {
486                return getTable();
487        }
488
489        public Integer getColumnIndex() {
490                return columnIndex;
491        }
492
493        public boolean isVariant() {
494                return isVariant;
495        }
496
497        public void setVariant(boolean isVariant) {
498                this.isVariant = isVariant;
499        }
500
501        public String getDisplayName() {
502                return displayName;
503        }
504
505        public void setDisplayName(String displayName) {
506                this.displayName = displayName;
507        }
508
509        public void setExpandStar(boolean expandStar) {
510                this.expandStar = expandStar;
511        }
512
513        public boolean isExpandStar() {
514                return expandStar;
515        }
516
517        public boolean isPseduo() {
518                return isPseduo;
519        }
520
521        public void setPseduo(boolean isPseduo) {
522                this.isPseduo = isPseduo;
523        }
524
525        public void notBindStarLinkColumn(boolean notBindStarColumn) {
526                this.notBindStarColumn = notBindStarColumn;
527        }
528        
529        public boolean isNotBindStarLinkColumn() {
530                return notBindStarColumn;
531        }
532
533    public Boolean getPrimaryKey() {
534        return primaryKey;
535    }
536
537    public void setPrimaryKey(Boolean primaryKey) {
538        this.primaryKey = primaryKey;
539    }
540
541    public Boolean getUnqiueKey() {
542        return unqiueKey;
543    }
544
545    public void setUnqiueKey(Boolean unqiueKey) {
546        this.unqiueKey = unqiueKey;
547    }
548
549    public Boolean getIndexKey() {
550        return indexKey;
551    }
552
553    public void setIndexKey(Boolean indexKey) {
554        this.indexKey = indexKey;
555    }
556
557    public Boolean getForeignKey() {
558        return foreignKey;
559    }
560
561    public void setForeignKey(Boolean foreignKey) {
562        this.foreignKey = foreignKey;
563    }
564
565    public String getDataType() {
566        return dataType;
567    }
568
569    public void setDataType(String dataType) {
570        this.dataType = dataType;
571    }
572    
573    @Override
574    public String toString() {
575        return this.name;
576    }
577
578        public void setColumnIndex(int columnIndex) {
579                this.columnIndex = columnIndex;
580        }
581
582        public boolean isStruct() {
583                return isStruct;
584        }
585
586        public void setStruct(boolean isStruct) {
587                this.isStruct = isStruct;
588        }
589
590        public List<Transform> getTransforms() {
591                return transforms;
592        }
593
594        public void setTransform(Transform transform) {
595                if(transform==null) {
596                        return;
597                }
598                if(transforms == null) {
599                        transforms = new ArrayList<Transform>();
600                }
601                if(!transforms.contains(transform)) {
602                        transforms.add(transform);
603                }
604        }
605
606        public List<Object> getCandidateParents() {
607                return candidateParents;
608        }
609
610        public void setCandidateParents(List<Object> candidateParents) {
611                if (this.candidateParents == null) {
612                        this.candidateParents = candidateParents;
613                }
614                else {
615                        this.candidateParents.addAll(candidateParents);
616                }
617        }
618
619        public TableColumn getSourceColumn() {
620                return sourceColumn;
621        }
622
623        public void setSourceColumn(TableColumn sourceColumn) {
624                this.sourceColumn = sourceColumn;
625        }
626}