001
002package gudusoft.gsqlparser.dlineage.dataflow.model;
003
004import gudusoft.gsqlparser.EDbVendor;
005import gudusoft.gsqlparser.ETableEffectType;
006import gudusoft.gsqlparser.ETableKind;
007import gudusoft.gsqlparser.ETableSource;
008import gudusoft.gsqlparser.TCustomSqlStatement;
009import gudusoft.gsqlparser.TSourceToken;
010import gudusoft.gsqlparser.dlineage.util.DlineageUtil;
011import gudusoft.gsqlparser.dlineage.util.LegacyDlineageWireNamesV1;
012import gudusoft.gsqlparser.dlineage.util.TableIdentityKey;
013import gudusoft.gsqlparser.dlineage.util.Pair;
014import gudusoft.gsqlparser.dlineage.util.Pair3;
015import gudusoft.gsqlparser.nodes.TFunctionCall;
016import gudusoft.gsqlparser.nodes.TObjectName;
017import gudusoft.gsqlparser.nodes.TTable;
018import gudusoft.gsqlparser.sqlenv.*;
019import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
020import gudusoft.gsqlparser.stmt.TCreateViewSqlStatement;
021import gudusoft.gsqlparser.util.Logger;
022import gudusoft.gsqlparser.util.LoggerFactory;
023import gudusoft.gsqlparser.util.SQLUtil;
024
025import java.io.ByteArrayInputStream;
026import java.io.IOException;
027import java.util.*;
028
029public class Table {
030
031        private static final Logger logger = LoggerFactory.getLogger(Table.class);
032
033        private long id;
034        protected String server;
035        protected String database;
036        protected String schema;
037        protected String name;
038        protected String displayName;
039        protected String fullName;
040        // Captured in the occurrence's context, before rendering/quote removal.
041        private QualifiedCanonKey identifierKey;
042
043        public QualifiedCanonKey getIdentifierKey() {
044                return identifierKey;
045        }
046
047        private void captureIdentifierKey(String lexicalName, TSQLTable resolved) {
048                if (ModelBindingManager.getGlobalVendor() != EDbVendor.dbvbigquery) return;
049                identifierKey = resolved == null || !inSpelledProject(lexicalName, resolved)
050                                ? (QualifiedCanonKey) DlineageUtil.getTableIdentityKey(lexicalName)
051                                : IdentifierService.qualifiedCanonKeyFromStoredStatic(EDbVendor.dbvbigquery,
052                                                ESQLDataObjectType.dotTable, Arrays.asList(resolved.getSchema().getCatalog().getName(),
053                                                                resolved.getSchema().getName(), resolved.getName()));
054        }
055
056        /**
057         * Whether the catalog's table is in the project the name spells, when it spells one. The catalog lends a
058         * reference that names a project the columns of a table declared without one (Mantis 4751), not that table's
059         * identity: {@code db1.s1.t1} and {@code db2.s1.t1} stay two tables beside the declared {@code s1.t1}.
060         */
061        private static boolean inSpelledProject(String lexicalName, TSQLTable resolved) {
062                List<String> segments = QualifiedNameHierarchy.split(EDbVendor.dbvbigquery, lexicalName);
063                if (segments.size() < 3) return true;
064                IdentifierService service = ModelBindingManager.getGlobalIdentifierService();
065                if (service == null) service = IdentifierService.defaultServiceFor(EDbVendor.dbvbigquery);
066                return service.canonKeyOf(segments.get(0), ESQLDataObjectType.dotCatalog, IdentifierInputForm.SQL_LEXICAL)
067                                .equals(service.canonKeyOf(resolved.getSchema().getCatalog().getName(), ESQLDataObjectType.dotCatalog,
068                                                IdentifierInputForm.CATALOG_STORED));
069        }
070
071        /**
072         * Binding identity, separate from legacy display getters: the table's identity under the
073         * analysis's identifier rules ({@link #getCanonicalIdentity()}) with the legacy folded name bound
074         * alongside, or the legacy name alone for a table the rules give no identity.
075         */
076        public Object getBindingKey() {
077                if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery) {
078                        return getIdentifierKey();
079                }
080                String legacyKey = DlineageUtil.getTableFullName(getName());
081                QualifiedCanonKey identity = getCanonicalIdentity();
082                return identity == null ? legacyKey : new TableIdentityKey(identity, legacyKey);
083        }
084
085        /**
086         * The form each level of the name that the reference did not spell arrived in, recorded where
087         * the level is produced. Null is a SQL spelling: a session default, a {@code USE}, a resolver's
088         * stamp, a file-path option. {@code CATALOG_STORED} is a name the catalog holds: the schema a
089         * catalog search found an unqualified name in ({@link TObjectName#getImplictSchemaForm()}), or a
090         * catalog or synonym match in {@link #fillSchemaInfo()}. The identity is minted from these, never
091         * by re-reading the display name, which shows a stored name bare.
092         */
093        private IdentifierInputForm databaseForm;
094        private IdentifierInputForm schemaForm;
095        /** How many trailing segments of the composed name the reference itself spelled; -1 until composed. */
096        private int spelledSegments = -1;
097        /** The identity last minted, and the name it was minted for. */
098        private QualifiedCanonKey mintedIdentity;
099        private String mintedFor;
100
101        private void assignDatabase(String database, IdentifierInputForm form) {
102                this.database = database;
103                // The global default is handed over as the very object ModelBindingManager holds (by
104                // DlineageUtil.getTableDatabase, or through the session default the bootstrap set from it),
105                // and a stored one keeps its form: a reference check, provenance rather than name equality.
106                if (database != null && form == null && database == ModelBindingManager.getGlobalDatabase()) {
107                        form = ModelBindingManager.getGlobalDatabaseForm();
108                }
109                this.databaseForm = database == null ? null : form;
110        }
111
112        private void assignSchema(String schema, IdentifierInputForm form) {
113                this.schema = schema;
114                if (schema != null && form == null && schema == ModelBindingManager.getGlobalSchema()) {
115                        form = ModelBindingManager.getGlobalSchemaForm();
116                }
117                this.schemaForm = schema == null ? null : form;
118        }
119
120        private static IdentifierInputForm implicitSchemaForm(TObjectName name) {
121                return name != null && name.getImplictSchemaForm() == IdentifierInputForm.CATALOG_STORED
122                                ? IdentifierInputForm.CATALOG_STORED : null;
123        }
124
125        private static IdentifierInputForm implicitDatabaseForm(TObjectName name) {
126                return name != null && name.getImplictDatabaseForm() == IdentifierInputForm.CATALOG_STORED
127                                ? IdentifierInputForm.CATALOG_STORED : null;
128        }
129
130        /**
131         * A catalog's stored name as it appears in the composed name: stored, unless the caller already
132         * delimited it ({@code SQLUtil.quoteDottedName} for a stored name with a dot), which makes it the SQL
133         * spelling of that name.
134         */
135        private static IdentifierInputForm storedUnlessDelimited(ESQLDataObjectType role, String name) {
136                if (name == null) {
137                        return null;
138                }
139                IdentifierService service = ModelBindingManager.getGlobalIdentifierService();
140                IdentifierProfile profile = service != null ? service.getProfile()
141                                : IdentifierProfile.forVendor(ModelBindingManager.getGlobalVendor(), IdentifierProfile.VendorFlags.defaults());
142                return IdentifierCodec.isQuoted(profile, role, name) ? null : IdentifierInputForm.CATALOG_STORED;
143        }
144
145        /**
146         * The table's identity under the analysis's identifier rules: BigQuery's captured key, else a key
147         * minted from the composed name's segments, each read in the form it arrived in. Levels the
148         * reference spelled, and levels a SQL spelling supplied, are read as SQL; a level the catalog
149         * supplied is read as the stored name it is. Minted during analysis and kept until the name
150         * changes, so the output merge sees the identity the model was bound under. Null for a name the
151         * rules give no identity: a database link, a shape the vendor's hierarchy cannot split, or a
152         * segment that is not an identifier.
153         */
154        public QualifiedCanonKey getCanonicalIdentity() {
155                if (identifierKey != null) {
156                        return identifierKey;
157                }
158                if (name == null) {
159                        return null;
160                }
161                if (mintedFor != name) { // reference check: re-mint only when the name itself changed
162                        mintedIdentity = mintIdentity();
163                        mintedFor = name;
164                }
165                return mintedIdentity;
166        }
167
168        /**
169         * The table's identity read in another object role of the same level ({@code dotSynonyms} for a name that may
170         * name a synonym): the same segments, forms and rules as {@link #getCanonicalIdentity()}, the last segment under
171         * that role's rules. A per-object profile may give synonyms rules of their own, so a synonym is matched in its
172         * own role. Null when the rules give the name no identity.
173         */
174        public QualifiedCanonKey getCanonicalIdentityAs(ESQLDataObjectType role) {
175                if (role == null || role == ESQLDataObjectType.dotTable) {
176                        return getCanonicalIdentity();
177                }
178                return name == null ? null : mintIdentity(role);
179        }
180
181        private QualifiedCanonKey mintIdentity() {
182                return mintIdentity(ESQLDataObjectType.dotTable);
183        }
184
185        private QualifiedCanonKey mintIdentity(ESQLDataObjectType role) {
186                IdentifierService service = ModelBindingManager.getGlobalIdentifierService();
187                if (service == null || subType == SubType.dblink
188                                || (ModelBindingManager.get() != null && ModelBindingManager.get().isDblinkTable(name))) {
189                        return null;
190                }
191                IdentifierProfile profile = service.getProfile();
192                EDbVendor vendor = profile.getVendor();
193                if (!QualifiedNameHierarchy.hasHierarchyMapping(vendor, role)) {
194                        return null;
195                }
196                List<QualifiedNameHierarchy.Segment> plan = QualifiedNameHierarchy.plan(vendor, role,
197                                QualifiedNameHierarchy.split(vendor, DlineageUtil.getTableFullNameAsWritten(name)));
198                if (plan == null) {
199                        return null;
200                }
201                int spelled = spelledSegments < 0 ? plan.size() : Math.min(spelledSegments, plan.size());
202                List<String> texts = new ArrayList<String>(plan.size());
203                List<IdentifierInputForm> forms = new ArrayList<IdentifierInputForm>(plan.size());
204                for (int i = 0; i < plan.size(); i++) {
205                        QualifiedNameHierarchy.Segment segment = plan.get(i);
206                        IdentifierInputForm form = IdentifierInputForm.SQL_LEXICAL;
207                        if (i < plan.size() - spelled && !IdentifierCodec.isQuoted(profile, segment.getType(), segment.getText())) {
208                                IdentifierInputForm produced = segment.getType() == ESQLDataObjectType.dotSchema ? schemaForm
209                                                : segment.getType() == ESQLDataObjectType.dotCatalog ? databaseForm : null;
210                                if (produced == IdentifierInputForm.CATALOG_STORED) {
211                                        form = IdentifierInputForm.CATALOG_STORED;
212                                }
213                        }
214                        if (form == IdentifierInputForm.SQL_LEXICAL
215                                        && !IdentifierCodec.isVendorValidSpelling(profile, segment.getType(), segment.getText())) {
216                                return null;
217                        }
218                        texts.add(segment.getText());
219                        forms.add(form);
220                }
221                try {
222                        return service.qualifiedCanonKeyFromSegments(texts, forms, role);
223                } catch (RuntimeException noIdentity) {
224                        return null;
225                }
226        }
227        protected Set<String> alias = new LinkedHashSet<String>();
228        protected Set<Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>>> positions = new LinkedHashSet<>();
229        protected String parent;
230        protected Pair3<Long, Long, String> startPosition;
231        protected Pair3<Long, Long, String> endPosition;
232        private List<TableColumn> columns = new ArrayList<TableColumn>();
233        private boolean subquery = false;
234
235        /**
236         * Set when an unqualified one-part name matches the same base table in two or
237         * more schemas of the (otherwise unambiguous) default catalog and the default
238         * schema holds no such object. In that case GSP must NOT fabricate a
239         * non-existent default-schema object; it leaves the reference unqualified and
240         * exposes the candidate set instead. See
241         * docs/tmp/unqualified-name-default-schema-masks-cross-schema-ambiguity.md.
242         */
243        private boolean ambiguousUnqualifiedTable = false;
244        private List<String> candidateTables;
245
246        private TTable tableObject;
247        private TObjectName viewName;
248        private boolean isCreateTable;
249        private boolean isView;
250        private boolean isPseudo;
251        private boolean isExternal;
252        private boolean isStage;
253        private boolean isSequence;
254        private boolean isPath;
255        private boolean isStream;
256        private boolean isVariable;
257        private boolean isConstant;
258        private boolean isTarget;
259        private String procedureId;
260        private boolean isDatabase;
261        private boolean isSchema;
262        private boolean isDataSource;
263        private String location;
264        private String dataSourceName;
265        private String pathUri;
266        private String fileType;
267        private String fileFormat;
268        private List<Process> processes;
269        private SubType subType;
270        private String starStmt;
271        private boolean hasSQLEnv = false;
272        private String currentAlias;
273    private boolean isDetermined;
274
275        private boolean fromDDL;
276
277        /**
278         * How this endpoint was introduced in the analyzed SQL. The create-effect values
279         * (CREATE_TABLE_DDL / SELECT_INTO / CTAS / CLONE_TABLE) are set as parse facts at
280         * build time, independent of {@link #isDetermined()} — unlike {@link #fromDDL}.
281         * Strictly additive; never back-fills {@link #subType}. See
282         * docs/tmp/dlineage-authoritative-endpoint-classification.md.
283         */
284        private EndpointIntroduction endpointIntroduction = EndpointIntroduction.UNKNOWN;
285
286        /**
287         * True when this endpoint is created by the analyzed SQL (CREATE TABLE / SELECT
288         * INTO / CTAS / clone target), set regardless of schema resolution.
289         */
290        private boolean createdInSql;
291
292        /**
293         * Pipe-joined, outermost-first list of the identifier segments of {@link #name} that
294         * overlap an unresolved dynamic-SQL hole ({@code "db"}, {@code "db|name"}, ...), or
295         * {@code null} when this endpoint is not runtime-templated. Written ONLY from inside a
296         * dynamic-fold context by the analyzer, so a static reference to a legally delimited
297         * {@code [weird@name]} is never marked. See
298         * docs/tmp/dynamic-template-endpoint-provenance.md.
299         */
300        private String templatedParts;
301
302        /**
303         * True when the hole covers the LEADING character of the object-name segment — i.e.
304         * the {@code #}/{@code ##}/{@code @} prefix that would otherwise classify this endpoint
305         * as a temp table / table variable is itself runtime text, so that classification is a
306         * fold artifact rather than a fact. Drives the {@link EndpointKind#DYNAMIC_TEMPLATE}
307         * precedence in {@link #getEndpointKind()}.
308         */
309        private boolean templatedIdentity;
310
311        private TableRelationRows relationRows = new TableRelationRows(this);
312
313        private TCustomSqlStatement stmt;
314
315        public Table(TCustomSqlStatement stmt, TObjectName viewName) {
316                this(viewName);
317                this.stmt = stmt;
318                if (stmt != null) {
319                        setPosition(stmt);
320                }
321        }
322
323        public TCustomSqlStatement getSqlStatement() {
324                return stmt;
325        }
326        
327        public Table(TTable table, String tableName) {
328                if (table == null) {
329                        throw new IllegalArgumentException("Table arguments can't be null.");
330                }
331
332                id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
333
334                this.tableObject = table;
335
336                setPosition(table);
337
338                this.name = tableName;
339                this.fullName = tableName;
340                setAlias(table.getAliasName());
341                setCurrentAlias(table.getAliasName());
342                
343                captureIdentifierKey(tableName, null);
344
345                if (name.startsWith("`") && name.endsWith("`") && name.indexOf(".") != -1
346                                && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) {
347                        this.name = SQLUtil.trimColumnStringQuote(name);
348                        this.fullName = SQLUtil.trimColumnStringQuote(fullName);
349                }
350
351                if (table.getSubquery() != null) {
352                        subquery = true;
353                }
354
355                EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
356                boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
357                boolean supportSchema = TSQLEnv.supportSchema(vendor);
358
359                if (table.getTableName().getDblink() == null) {
360                        this.server = DlineageUtil.getTableServer(table.getTableName().toString());
361                        assignDatabase(DlineageUtil.getTableDatabase(table.getTableName().toString()), null);
362                        assignSchema(DlineageUtil.getTableSchema(table.getTableName().toString()), null);
363                }
364                else {
365                        if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
366                                this.server = getDefaultServer();
367                        }
368                        assignDatabase(LegacyDlineageWireNamesV1.segment(vendor, ESQLDataObjectType.dotDblink, table.getTableName().getDblink().toString()), null);
369                        if (!SQLUtil.isEmpty(table.getTableName().getSchemaString())) {
370                                assignSchema(table.getTableName().getSchemaString(), null);
371                        }
372                        this.subType = SubType.dblink;
373                        this.name = this.name.replace(table.getTableName().getDblink().toString(), this.database);
374                        ModelBindingManager.get().appendDblinkTable(this.name);
375                        return;
376                }
377
378                fillSchemaInfo();
379
380                if (SQLUtil.isEmpty(this.database)) {
381                        assignDatabase(DlineageUtil.getTableDatabase(table.getTableName().toString()), null);
382                        if (SQLUtil.isEmpty(this.database) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
383                                        && !SQLUtil.isEmpty(table.getTableName().getImplictDatabaseString())) {
384                                assignDatabase(table.getTableName().getImplictDatabaseString(), implicitDatabaseForm(table.getTableName()));
385                        }
386                }
387
388                if (SQLUtil.isEmpty(this.schema)) {
389                        assignSchema(DlineageUtil.getTableSchema(table.getTableName().toString()), null);
390                        if (SQLUtil.isEmpty(this.schema) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
391                                        && !SQLUtil.isEmpty(implicitSchemaOf(table.getTableName()))) {
392                                assignSchema(implicitSchemaOf(table.getTableName()), implicitSchemaForm(table.getTableName()));
393                        }
394                }
395
396                if (!SQLUtil.isEmpty(this.database) && SQLUtil.isEmpty(this.schema)) {
397                        if (!SQLUtil.isEmpty(implicitSchemaOf(table.getTableName()))) {
398                                assignSchema(implicitSchemaOf(table.getTableName()), implicitSchemaForm(table.getTableName()));
399                        }
400                }
401
402                if (table.getTableType() == ETableSource.function) {
403                        subType = SubType.function;
404                }
405
406                if (!supportCatalog) {
407                        assignDatabase(null, null);
408                } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
409                        assignDatabase(getDefaultDatabase(), null);
410                }
411
412                if (!supportSchema) {
413                        assignSchema(null, null);
414                } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
415                        assignSchema(getDefaultSchema(), null);
416                }
417
418                updateTableName(supportCatalog, supportSchema);
419
420                if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
421                        this.server = getDefaultServer();
422                }
423        }
424
425        public Table(TTable table) {
426                if (table == null) {
427                        throw new IllegalArgumentException("Table arguments can't be null.");
428                }
429
430                id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
431
432                this.tableObject = table;
433
434                setPosition(table);
435
436                if (table.getLinkTable() != null) {
437                        this.fullName = table.getLinkTable().getFullName();
438                        this.name = table.getLinkTable().getName();
439                        setAlias(table.getName());
440                        setCurrentAlias(table.getName());
441                } else if (table.getTableType() == ETableSource.function && table.getFuncCall() != null) {
442                        TFunctionCall function = table.getFuncCall();
443                        this.fullName = function.getFunctionName().toString();
444                        this.name = function.getFunctionName().toString();
445                        setAlias(table.getAliasName());
446                        setCurrentAlias(table.getAliasName());
447                } else {
448                        if (table.getSubquery() != null && table.getAliasClause() != null) {
449                                this.name = table.getAliasClause().getAliasName().toString();
450                                this.subType = SubType.alias_table;
451                        } else if (table.getTableName() != null) {
452                                this.name = table.getTableName().toString();
453                        } else {
454                                this.name = table.getName();
455                        }
456
457                        this.fullName = table.getFullName();
458                        if (this.fullName == null) {
459                                this.fullName = this.name;
460                        }
461
462                        setAlias(table.getAliasName());
463                        setCurrentAlias(table.getAliasName());
464                }
465
466                captureIdentifierKey(table.getLinkTable() == null ? this.name
467                                : table.getLinkTable().getTableName().toString(), table.getResolvedTable());
468
469                if (name.startsWith("`") && name.endsWith("`") && name.indexOf(".") != -1
470                                && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) {
471                        this.name = SQLUtil.trimColumnStringQuote(name);
472                        this.fullName = SQLUtil.trimColumnStringQuote(fullName);
473                }
474
475                if (table.getSubquery() != null) {
476                        subquery = true;
477                }
478
479                EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
480                boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
481                boolean supportSchema = TSQLEnv.supportSchema(vendor);
482
483                if (table.getTableName().getDblink() == null) {
484                        this.server = DlineageUtil.getTableServer(table.getTableName().toString());
485                        assignDatabase(DlineageUtil.getTableDatabase(table.getTableName().toString()), null);
486                        assignSchema(DlineageUtil.getTableSchema(table.getTableName().toString()), null);
487                }
488                else {
489                        if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
490                                this.server = getDefaultServer();
491                        }
492                        assignDatabase(LegacyDlineageWireNamesV1.segment(vendor, ESQLDataObjectType.dotDblink, table.getTableName().getDblink().toString()), null);
493                        if (!SQLUtil.isEmpty(table.getTableName().getSchemaString())) {
494                                assignSchema(table.getTableName().getSchemaString(), null);
495                        }
496                        this.subType = SubType.dblink;
497                        this.name = this.name.replace(table.getTableName().getDblink().toString(), this.database);
498                        ModelBindingManager.get().appendDblinkTable(this.name);
499                        return;
500                }
501
502                fillSchemaInfo();
503
504                if (SQLUtil.isEmpty(this.database)) {
505                        assignDatabase(DlineageUtil.getTableDatabase(table.getTableName().toString()), null);
506                        if (SQLUtil.isEmpty(this.database) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
507                                        && !SQLUtil.isEmpty(table.getTableName().getImplictDatabaseString())) {
508                                assignDatabase(table.getTableName().getImplictDatabaseString(), implicitDatabaseForm(table.getTableName()));
509                        }
510                }
511
512                if (SQLUtil.isEmpty(this.schema)) {
513                        assignSchema(DlineageUtil.getTableSchema(table.getTableName().toString()), null);
514                        if (SQLUtil.isEmpty(this.schema) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
515                                        && !SQLUtil.isEmpty(implicitSchemaOf(table.getTableName()))) {
516                                assignSchema(implicitSchemaOf(table.getTableName()), implicitSchemaForm(table.getTableName()));
517                        }
518                }
519
520                if (!SQLUtil.isEmpty(this.database) && SQLUtil.isEmpty(this.schema)) {
521                        if (!SQLUtil.isEmpty(implicitSchemaOf(table.getTableName()))) {
522                                assignSchema(implicitSchemaOf(table.getTableName()), implicitSchemaForm(table.getTableName()));
523                        }
524                }
525
526                if (table.getTableType() == ETableSource.function) {
527                        subType = SubType.function;
528                }
529
530                if (!supportCatalog) {
531                        assignDatabase(null, null);
532                } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
533                        assignDatabase(getDefaultDatabase(), null);
534                }
535
536                if (!supportSchema) {
537                        assignSchema(null, null);
538                } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
539                        assignSchema(getDefaultSchema(), null);
540                }
541
542                updateTableName(supportCatalog, supportSchema);
543
544                if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
545                        this.server = getDefaultServer();
546                }
547        }
548
549        public void addColumnsFromSQLEnv(){
550                TSQLEnv sqlEnv = ModelBindingManager.getGlobalSQLEnv();
551                if (sqlEnv == null) {
552                        return;
553                }
554                TSQLTable tsqlTable = sqlEnv.searchTable(ModelFactory.getQualifiedTableName(this));
555                if (tsqlTable != null && tsqlTable.getColumnList()!=null && !tsqlTable.getColumnList().isEmpty()) {
556                        for(TSQLColumn tsqlColumn: tsqlTable.getColumnList()){
557                                TObjectName columnName;
558                                if (tsqlColumn.isUnresolvedStar()) {
559                                        columnName = DlineageUtil.syntheticStarName();
560                                } else {
561                                        columnName = DeclaredColumnName.ofCatalog(tsqlColumn.getName(),
562                                                        TSQLEnv.toLexical(ModelBindingManager.getGlobalVendor(),
563                                                                        ESQLDataObjectType.dotColumn, tsqlColumn.getName(),
564                                                                        gudusoft.gsqlparser.sqlenv.IdentifierInputForm.CATALOG_STORED));
565                                }
566                                TableColumn column = new TableColumn(this, columnName);
567                                column.name = tsqlColumn.getName();
568                        }
569                        this.setCreateTable(true);
570                        this.setHasSQLEnv(true);
571                }
572        }
573
574        private TSQLEnv metadataEnvironment() {
575                if (tableObject != null && tableObject.getSqlEnv() != null && ModelBindingManager.isScopedTemporary(tableObject)) {
576                        // SELECT INTO introduces this spelling; its inferred shape does not qualify the name.
577                        if (tableObject.getEffectType() == ETableEffectType.tetSelectInto) return null;
578                        return tableObject.getSqlEnv();
579                }
580                return ModelBindingManager.getGlobalSQLEnv();
581        }
582
583        private void fillSchemaInfo() {
584                Stack<TCustomSqlStatement> stmtStack = ModelBindingManager.getGlobalStmtStack();
585                if (!stmtStack.isEmpty()) {
586                        TCustomSqlStatement currentStmt = stmtStack.peek();
587                        TCustomSqlStatement stmt = DlineageUtil.getTopStmt(stmtStack.peek());
588                        
589                        String sqlComment = null;
590                        try {
591                                sqlComment = stmt.getCommentBeforeNode();
592                        } catch (Exception e) {
593                        }
594                        if (!SQLUtil.isEmpty(sqlComment) && (sqlComment.indexOf("db") != -1 || sqlComment.indexOf("schema") != -1)) {
595                                Properties properties = new Properties();
596                                try {
597                                        properties.load(
598                                                        new ByteArrayInputStream(sqlComment.replace("--", "").trim().replace(",", "\n").getBytes()));
599                                        if (SQLUtil.isEmpty(this.server) && properties.containsKey("db-instance")) {
600                                                this.server = properties.getProperty("db-instance");
601                                        }
602                                        if (SQLUtil.isEmpty(this.database) && properties.containsKey("db")) {
603                                                assignDatabase(properties.getProperty("db"), null);
604                                                if(this.database.indexOf(".")!=-1) {
605                                                        assignDatabase(SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotCatalog, this.database), null);
606                                                }
607                                        }
608                                        if (SQLUtil.isEmpty(this.schema) && properties.containsKey("schema")) {
609                                                assignSchema(properties.getProperty("schema"), null);
610                                                if(this.schema.indexOf(".")!=-1) {
611                                                        assignSchema(SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotSchema, this.schema), null);
612                                                }
613                                        }
614                                } catch (IOException e) {
615                                        logger.error("load sql comment properties failed.", e);
616                                }
617                        }
618                        
619                        if(stmt instanceof TCreateTableSqlStatement) {
620                                TCreateTableSqlStatement createTableStmt = (TCreateTableSqlStatement)stmt;
621                                if (createTableStmt.getTableKinds() != null 
622                                                && (createTableStmt.getTableKinds().contains(ETableKind.etkTemporary) 
623                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkTemp)
624                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkLocalTemporary)
625                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkLocalTemp)
626                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkGlobalTemporary)
627                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkGlobalTemp)
628                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkScopedTemporary))) {
629                                        if (this.tableObject == createTableStmt.getTargetTable()) {
630                                                setSubType(SubType.temp_table);
631                                                return;
632                                        }
633                                }
634                        }
635                        
636                        if(currentStmt instanceof TCreateTableSqlStatement) {
637                                TCreateTableSqlStatement createTableStmt = (TCreateTableSqlStatement)currentStmt;
638                                if (createTableStmt.getTableKinds() != null 
639                                                && (createTableStmt.getTableKinds().contains(ETableKind.etkTemporary) 
640                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkTemp)
641                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkLocalTemporary)
642                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkLocalTemp)
643                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkGlobalTemporary)
644                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkGlobalTemp)
645                                                                || createTableStmt.getTableKinds().contains(ETableKind.etkScopedTemporary))) {
646                                        if (this.tableObject == createTableStmt.getTargetTable()) {
647                                                setSubType(SubType.temp_table);
648                                                return;
649                                        }
650                                }
651                        }
652                        
653                        if(stmt instanceof TCreateViewSqlStatement) {
654                                TCreateViewSqlStatement createViewStmt = (TCreateViewSqlStatement)stmt;
655                                if (createViewStmt.getTableKind() != null 
656                                                && (createViewStmt.getTableKind() == (ETableKind.etkTemporary) 
657                                                                || createViewStmt.getTableKind() == (ETableKind.etkTemp)
658                                                                || createViewStmt.getTableKind() == (ETableKind.etkLocalTemporary)
659                                                                || createViewStmt.getTableKind() == (ETableKind.etkLocalTemp)
660                                                                || createViewStmt.getTableKind() == (ETableKind.etkGlobalTemporary)
661                                                                || createViewStmt.getTableKind() == (ETableKind.etkGlobalTemp))) {
662                                        if (this.viewName == createViewStmt.getViewName()) {
663                                                setSubType(SubType.temp_table);
664                                                return;
665                                        }
666                                }
667                        }
668                        
669                        if(currentStmt instanceof TCreateViewSqlStatement) {
670                                TCreateViewSqlStatement createViewStmt = (TCreateViewSqlStatement)currentStmt;
671                                if (createViewStmt.getTableKind() != null 
672                                                && (createViewStmt.getTableKind() == (ETableKind.etkTemporary) 
673                                                                || createViewStmt.getTableKind() == (ETableKind.etkTemp)
674                                                                || createViewStmt.getTableKind() == (ETableKind.etkLocalTemporary)
675                                                                || createViewStmt.getTableKind() == (ETableKind.etkLocalTemp)
676                                                                || createViewStmt.getTableKind() == (ETableKind.etkGlobalTemporary)
677                                                                || createViewStmt.getTableKind() == (ETableKind.etkGlobalTemp))) {
678                                        if (this.viewName == createViewStmt.getViewName()) {
679                                                setSubType(SubType.temp_table);
680                                                return;
681                                        }
682                                }
683                        }
684                }
685                
686
687                if (SQLUtil.isEmpty(this.database) || this.database.equals(ModelBindingManager.getGlobalDatabase())) {
688                        TSQLEnv sqlEnv = metadataEnvironment();
689                        if (sqlEnv == null) {
690                                return;
691                        }
692                        int occurrence = 0;
693                        int maxPriority = -1;
694                        String tableDatabase = null;
695                        String tableSchema = null;
696                        Map<Integer, List<String>> databaseSchemasMap = new HashMap<Integer, List<String>>();
697                        // Every cross-schema hit for the bare name, grouped by priority, so a name
698                        // that is ambiguous across schemas (and which the rules below cannot resolve
699                        // to a single winner) can be surfaced as a candidate set instead of being
700                        // stamped with a non-existent default schema.
701                        Map<Integer, List<String[]>> candidatesByPriority = new TreeMap<Integer, List<String[]>>();
702                        for (TSQLCatalog catalog : sqlEnv.getCatalogList()) {
703                                if (SQLUtil.isEmpty(schema) || this.schema.equals(ModelBindingManager.getGlobalSchema())) {
704                                        for (TSQLSchema schema : catalog.getSchemaList()) {
705                                                TSQLSchemaObject tsqlTable = schema.findTable(DlineageUtil.getSimpleTableName(this.name));
706                                                if(tsqlTable == null){
707                                                        tsqlTable = schema.findSynonyms(DlineageUtil.getSimpleTableName(this.name));
708                                                }
709                                                if (tsqlTable != null) {
710                                                        occurrence += 1;
711                                                        String candDb = quoteDottedSegment(catalog.getName());
712                                                        String candSchema = quoteDottedSegment(schema.getName());
713                                                        String candName = candDb + "." + candSchema + "."
714                                                                        + DlineageUtil.getSimpleTableName(this.name);
715                                                        int candPriority = tsqlTable.getPriority();
716                                                        if (!candidatesByPriority.containsKey(candPriority)) {
717                                                                candidatesByPriority.put(candPriority, new ArrayList<String[]>());
718                                                        }
719                                                        candidatesByPriority.get(candPriority).add(new String[] { candDb, candSchema, candName, schema.getNameKeepCase() });
720                                                        if (tsqlTable.getPriority() > maxPriority
721                                                                        || (maxPriority > 0 && tsqlTable.getPriority() == maxPriority)) {
722                                                                tableDatabase = catalog.getName();
723                                                                if(tableDatabase.indexOf(".")!=-1) {
724                                                                        tableDatabase = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotCatalog, tableDatabase);
725                                                                }
726                                                                tableSchema = schema.getName();
727                                                                if(tableSchema.indexOf(".")!=-1) {
728                                                                        tableSchema = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotSchema, tableSchema);
729                                                                }
730                                                                maxPriority = tsqlTable.getPriority();
731                                                                if(!databaseSchemasMap.containsKey(maxPriority)) {
732                                                                        databaseSchemasMap.put(maxPriority, new ArrayList<String>());
733                                                                }
734                                                                databaseSchemasMap.get(maxPriority).add(tableDatabase + "___" + tableSchema);
735                                                        }
736                                                }
737                                        }
738//                                      if (occurrence > 1) {
739//                                              break;
740//                                      }
741                                } else {
742                                        TSQLSchema schema = catalog.getSchema(this.schema, false);
743                                        if (schema != null) {
744                                                TSQLSchemaObject tsqlTable = schema.findTable(DlineageUtil.getSimpleTableName(this.name));
745                                                if(tsqlTable == null){
746                                                        tsqlTable = schema.findSynonyms(DlineageUtil.getSimpleTableName(this.name));
747                                                }
748                                                if (tsqlTable != null) {
749                                                        occurrence += 1;
750                                                        if (tsqlTable.getPriority() > maxPriority
751                                                                        || (maxPriority > 0 && tsqlTable.getPriority() == maxPriority)) {
752                                                                tableDatabase = catalog.getName();
753                                                                if(tableDatabase.indexOf(".")!=-1) {
754                                                                        tableDatabase = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotCatalog, tableDatabase);
755                                                                }
756                                                                tableSchema = schema.getName();
757                                                                if(tableSchema.indexOf(".")!=-1) {
758                                                                        tableSchema = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotSchema, tableSchema);
759                                                                }
760                                                                maxPriority = tsqlTable.getPriority();
761                                                                if(!databaseSchemasMap.containsKey(maxPriority)) {
762                                                                        databaseSchemasMap.put(maxPriority, new ArrayList<String>());
763                                                                }
764                                                                databaseSchemasMap.get(maxPriority).add(tableDatabase + "___" + tableSchema);
765                                                        }
766                                                }
767                                        }
768                                }
769                        }
770                        if (occurrence == 1 || (maxPriority > 0 && databaseSchemasMap.get(maxPriority).size() == 1)) {
771                                if (this.database == null && tableDatabase != null && !TSQLEnv.DEFAULT_DB_NAME.equalsIgnoreCase(tableDatabase)) {
772                                        assignDatabase(tableDatabase, storedUnlessDelimited(ESQLDataObjectType.dotCatalog, tableDatabase));
773                                }
774                                if (this.schema == null && tableSchema != null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equalsIgnoreCase(tableSchema)) {
775                                        assignSchema(tableSchema, storedUnlessDelimited(ESQLDataObjectType.dotSchema, tableSchema));
776                                }
777                        } else if ((this.schema == null || this.schema.equals(ModelBindingManager.getGlobalSchema()))
778                                        && !candidatesByPriority.isEmpty()) {
779                                // The existing rules could not pick a single winner and the schema is
780                                // still only a default (null or the global default). Look at the
781                                // top-priority group: when it holds two or more schemas AND none of
782                                // them is the default schema, the unqualified name is genuinely
783                                // ambiguous. Flag it and record the candidate set so the output layer
784                                // can surface the reference unqualified instead of fabricating a
785                                // non-existent default-schema object. A match in the default schema
786                                // (e.g. an existing dbo.orders) is left to win through the normal
787                                // stamping path.
788                                int maxCandPriority = Collections.max(candidatesByPriority.keySet());
789                                List<String[]> topCandidates = candidatesByPriority.get(maxCandPriority);
790                                String defaultSchema = getDefaultSchema();
791                                boolean defaultSchemaHasObject = false;
792                                for (String[] cand : topCandidates) {
793                                        if (cand[1] != null && DlineageUtil.sameName(ESQLDataObjectType.dotSchema, cand[3], defaultSchema)) {
794                                                defaultSchemaHasObject = true;
795                                                break;
796                                        }
797                                }
798                                if (!defaultSchemaHasObject && topCandidates.size() >= 2) {
799                                        List<String> candidates = new ArrayList<String>();
800                                        for (String[] cand : topCandidates) {
801                                                candidates.add(cand[2]);
802                                        }
803                                        this.candidateTables = candidates;
804                                        this.ambiguousUnqualifiedTable = true;
805                                }
806                        }
807                } else if (SQLUtil.isEmpty(this.schema) || this.schema.equals(ModelBindingManager.getGlobalSchema())) {
808                        TSQLEnv sqlEnv = metadataEnvironment();
809                        if (sqlEnv == null) {
810                                return;
811                        }
812                        int occurrence = 0;
813                        int maxPriority = -1;
814                        String tableDatabase = null;
815                        String tableSchema = null;
816                        Map<Integer, List<String>> databaseSchemasMap = new HashMap<Integer, List<String>>();
817                        TSQLCatalog catalog = sqlEnv.searchCatalog(this.database);
818                        if (catalog != null) {
819                                for (TSQLSchema schema : catalog.getSchemaList()) {
820                                        TSQLSchemaObject tsqlTable = schema.findTable(DlineageUtil.getSimpleTableName(this.name));
821                                        if(tsqlTable == null){
822                                                tsqlTable = schema.findSynonyms(DlineageUtil.getSimpleTableName(this.name));
823                                        }
824                                        if (tsqlTable != null) {
825                                                occurrence += 1;
826                                                if (tsqlTable.getPriority() > maxPriority
827                                                                || (maxPriority > 0 && tsqlTable.getPriority() == maxPriority)) {
828                                                        tableDatabase = catalog.getName();
829                                                        if (tableDatabase.indexOf(".") != -1) {
830                                                                tableDatabase = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotCatalog, tableDatabase);
831                                                        }
832                                                        tableSchema = schema.getName();
833                                                        if (tableSchema.indexOf(".") != -1) {
834                                                                tableSchema = SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(), ESQLDataObjectType.dotSchema, tableSchema);
835                                                        }
836                                                        maxPriority = tsqlTable.getPriority();
837                                                        if(!databaseSchemasMap.containsKey(maxPriority)) {
838                                                                databaseSchemasMap.put(maxPriority, new ArrayList<String>());
839                                                        }
840                                                        databaseSchemasMap.get(maxPriority).add(tableDatabase + "___" + tableSchema);
841                                                }
842                                        }
843                                }
844                        }
845
846                        if (occurrence == 1 || (maxPriority > 0 && databaseSchemasMap.get(maxPriority).size() == 1)) {
847                                if (this.database == null && tableDatabase != null && !TSQLEnv.DEFAULT_DB_NAME.equalsIgnoreCase(tableDatabase)) {
848                                        assignDatabase(tableDatabase, storedUnlessDelimited(ESQLDataObjectType.dotCatalog, tableDatabase));
849                                }
850                                if (this.schema == null && tableSchema != null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equalsIgnoreCase(tableSchema)) {
851                                        assignSchema(tableSchema, storedUnlessDelimited(ESQLDataObjectType.dotSchema, tableSchema));
852                                }
853                        }
854
855                        if (SQLUtil.isEmpty(this.schema) && !SQLUtil.isEmpty(this.database)) {
856                                assignSchema(getDefaultSchema(), null);
857                        }
858                }
859        }
860
861        public Table(TObjectName tableName) {
862                if (tableName == null) {
863                        throw new IllegalArgumentException("Table arguments can't be null.");
864                }
865
866                id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
867                
868                this.viewName = tableName;
869
870                setPosition(tableName);
871
872                this.fullName = tableName.toString();
873                this.name = tableName.toString();
874                if (SQLUtil.isEmpty(name)) {
875                        name = tableName.toString();
876                }
877
878                captureIdentifierKey(tableName.toString(), null);
879
880                if (name.startsWith("`") && name.endsWith("`") && name.indexOf(".") != -1
881                                && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) {
882                        this.name = SQLUtil.trimColumnStringQuote(name);
883                        this.fullName = SQLUtil.trimColumnStringQuote(fullName);
884                }
885
886                this.server = DlineageUtil.getTableServer(tableName.toString());
887                assignDatabase(DlineageUtil.getTableDatabase(tableName.toString()), null);
888                assignSchema(DlineageUtil.getTableSchema(tableName.toString()), null);
889
890                fillSchemaInfo();
891
892                EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
893                boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
894                boolean supportSchema = TSQLEnv.supportSchema(vendor);
895
896                if (supportCatalog && SQLUtil.isEmpty(this.database)) {
897                        assignDatabase(DlineageUtil.getTableDatabase(tableName.toString()), null);
898                        if (SQLUtil.isEmpty(this.database) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
899                                        && !SQLUtil.isEmpty(tableName.getImplictDatabaseString())) {
900                                assignDatabase(tableName.getImplictDatabaseString(), implicitDatabaseForm(tableName));
901                        }
902                }
903
904                if (supportSchema && SQLUtil.isEmpty(this.schema)) {
905                        assignSchema(DlineageUtil.getTableSchema(tableName.toString()), null);
906                        if (SQLUtil.isEmpty(this.schema) && ModelBindingManager.getGlobalOption().isShowImplicitSchema()
907                                        && !SQLUtil.isEmpty(implicitSchemaOf(tableName))) {
908                                assignSchema(implicitSchemaOf(tableName), implicitSchemaForm(tableName));
909                        }
910                }
911
912                if (supportSchema && !SQLUtil.isEmpty(this.database) && SQLUtil.isEmpty(this.schema)) {
913                        if (!SQLUtil.isEmpty(implicitSchemaOf(tableName))) {
914                                assignSchema(implicitSchemaOf(tableName), implicitSchemaForm(tableName));
915                        } else {
916                                assignSchema(getDefaultSchema(), null);
917                        }
918                }
919
920                if (!supportCatalog) {
921                        assignDatabase(null, null);
922                } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
923                        assignDatabase(getDefaultDatabase(), null);
924                }
925
926                if (!supportSchema) {
927                        assignSchema(null, null);
928                } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
929                        assignSchema(getDefaultSchema(), null);
930                }
931
932                updateTableName(supportCatalog, supportSchema);
933
934                if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
935                        this.server = getDefaultServer();
936                }
937        }
938
939        protected String getDefaultServer() {
940                String defaultServer = null;
941                if (ModelBindingManager.getGlobalSQLEnv() != null) {
942                        defaultServer = ModelBindingManager.getGlobalSQLEnv().getDefaultServerName();
943                }
944                if (!SQLUtil.isEmpty(defaultServer))
945                        return defaultServer;
946                return TSQLEnv.DEFAULT_SERVER_NAME;
947        }
948
949        /**
950         * The implicit schema stamped on {@code name}, or null when the statement
951         * being analyzed has no current schema because a Snowflake
952         * {@code USE <database>} unset it.
953         *
954         * <p>The stamp is applied by the resolvers from the TSQLEnv's default schema,
955         * which is batch-global: it holds the LAST {@code USE SCHEMA} of the whole
956         * script even for statements that run after a database switch invalidated it.
957         * Reading it unfiltered put the previous database's schema back on names the
958         * rest of this class had already left unqualified - a CREATE VIEW target
959         * reported {@code D2.S1.v} while its own source table reported
960         * {@code D2.DEFAULT.orders}. See GitHub #715 / MantisBT 4677.
961         */
962        private static String implicitSchemaOf(TObjectName name) {
963                if (name == null || ModelBindingManager.isGlobalSchemaUnset()) {
964                        return null;
965                }
966                return name.getImplictSchemaString();
967        }
968
969        protected String getDefaultSchema() {
970                return ModelBindingManager.getEffectiveDefaultSchema();
971        }
972
973        protected String getDefaultDatabase() {
974                return ModelBindingManager.getEffectiveDefaultDatabase();
975        }
976
977        /**
978         * @return true when this one-part reference is ambiguous across two or more
979         *         schemas and was intentionally left unqualified (no default schema
980         *         stamped). The candidates are available via {@link #getCandidateTables()}.
981         */
982        public boolean isAmbiguousUnqualifiedTable() {
983                return ambiguousUnqualifiedTable;
984        }
985
986        /**
987         * @return the qualified candidate table names (e.g. {@code testdb.s1.orders},
988         *         {@code testdb.s2.orders}) when this reference is ambiguous across
989         *         schemas, or null when the reference resolved normally.
990         */
991        public List<String> getCandidateTables() {
992                return candidateTables;
993        }
994
995        /**
996         * Quote a catalog/schema segment the same way the surrounding resolution code
997         * does (lines around the occurrence loop), so a name containing a dot is kept
998         * as a single delimited segment.
999         */
1000        private static String quoteDottedSegment(String segment) {
1001                if (segment != null && segment.indexOf('.') != -1) {
1002                        // catalog and schema segments share NAME_GROUP quote rules
1003                        return SQLUtil.quoteDottedName(ModelBindingManager.getGlobalOption().getVendor(),
1004                                        ESQLDataObjectType.dotSchema, segment);
1005                }
1006                return segment;
1007        }
1008
1009        /** Drops the model's position: a table no SQL named has none to publish. */
1010        void dropPosition() {
1011                startPosition = null;
1012                endPosition = null;
1013                positions.clear();
1014        }
1015
1016        public Table(String tableName) {
1017                if (tableName == null) {
1018                        throw new IllegalArgumentException("Table arguments can't be null.");
1019                }
1020
1021                id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
1022
1023                this.startPosition = new Pair3<Long, Long, String>(-1L, -1L, ModelBindingManager.getGlobalHash());
1024                this.endPosition = new Pair3<Long, Long, String>(-1L, -1L, ModelBindingManager.getGlobalHash());
1025
1026                this.fullName = tableName;
1027                this.name = tableName;
1028                if (SQLUtil.isEmpty(name)) {
1029                        name = tableName;
1030                }
1031
1032                captureIdentifierKey(tableName, null);
1033
1034                if (name.startsWith("`") && name.endsWith("`") && name.indexOf(".") != -1
1035                                && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) {
1036                        this.name = SQLUtil.trimColumnStringQuote(name);
1037                        this.fullName = SQLUtil.trimColumnStringQuote(fullName);
1038                }
1039
1040                EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
1041                boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
1042                boolean supportSchema = TSQLEnv.supportSchema(vendor);
1043
1044                fillSchemaInfo();
1045
1046                this.server = DlineageUtil.getTableServer(tableName);
1047
1048                if (supportCatalog && SQLUtil.isEmpty(this.database)) {
1049                        assignDatabase(DlineageUtil.getTableDatabase(tableName), null);
1050                }
1051                if (supportSchema && SQLUtil.isEmpty(this.schema)) {
1052                        assignSchema(DlineageUtil.getTableSchema(tableName), null);
1053                }
1054                if (supportSchema && !SQLUtil.isEmpty(this.database) && SQLUtil.isEmpty(this.schema)) {
1055                        assignSchema(getDefaultSchema(), null);
1056                }
1057
1058                if (!supportCatalog) {
1059                        assignDatabase(null, null);
1060                } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
1061                        assignDatabase(getDefaultDatabase(), null);
1062                }
1063
1064                if (!supportSchema) {
1065                        assignSchema(null, null);
1066                } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
1067                        assignSchema(getDefaultSchema(), null);
1068                }
1069
1070                updateTableName(supportCatalog, supportSchema);
1071
1072                if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
1073                        this.server = getDefaultServer();
1074                }
1075        }
1076
1077        protected void updateTableName(boolean supportCatalog, boolean supportSchema) {
1078                List<String> segments = SQLUtil.parseNames(this.name);
1079                spelledSegments = segments.size();
1080                this.name = segments.get(segments.size() - 1);
1081                if (supportCatalog && supportSchema) {
1082                        StringBuilder builder = new StringBuilder();
1083                        if (segments.size() > 2) {
1084                                builder.append(segments.get(segments.size() - 3)).append(".");
1085                        } else {
1086                                if (!SQLUtil.isEmpty(this.database) && !TSQLEnv.DEFAULT_DB_NAME.equalsIgnoreCase(this.database)) {
1087                                        builder.append(this.database).append(".");
1088                                }
1089                        }
1090                        if (segments.size() > 1) {
1091                                builder.append(segments.get(segments.size() - 2)).append(".");
1092                        } else {
1093                                if (builder.length() > 0) {
1094                                        if (this.schema == null) {
1095                                                if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) {
1096                                                        assignSchema("dbo", null);
1097                                                } else {
1098                                                        assignSchema(getDefaultSchema(), null);
1099                                                }
1100                                        }
1101                                        else if (TSQLEnv.DEFAULT_SCHEMA_NAME.equalsIgnoreCase(this.schema)
1102                                                        && ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) {
1103                                                assignSchema("dbo", null);
1104                                        }
1105                                        builder.append(this.schema).append(".");
1106                                } else {
1107                                        if (!SQLUtil.isEmpty(this.schema) && !TSQLEnv.DEFAULT_SCHEMA_NAME.equalsIgnoreCase(this.schema)) {
1108                                                builder.append(this.schema).append(".");
1109                                        }
1110                                }
1111                        }
1112                        builder.append(this.name);
1113                        this.name = builder.toString();
1114                } else if (supportCatalog) {
1115                        if (segments.size() > 1) {
1116                                this.name = segments.get(segments.size() - 2) + "." + this.name;
1117                        }
1118                        else {
1119                                if (!SQLUtil.isEmpty(this.database) && !TSQLEnv.DEFAULT_DB_NAME.equalsIgnoreCase(this.database)) {
1120                                        this.name = this.database + "." + this.name;
1121                                }
1122                        }
1123                } else if (supportSchema) {
1124                        if (segments.size() > 1) {
1125                                this.name = segments.get(segments.size() - 2) + "." + this.name;
1126                        } else {
1127                                if (!SQLUtil.isEmpty(this.schema) && !TSQLEnv.DEFAULT_SCHEMA_NAME.equalsIgnoreCase(this.schema)) {
1128                                        this.name = this.schema + "." + this.name;
1129                                }
1130                        }
1131                }
1132
1133                if (this.server != null && !this.server.equals(getDefaultServer())) {
1134                        this.name = this.server + "." + this.name;
1135                }
1136        }
1137
1138        public long getId() {
1139                return id;
1140        }
1141
1142        public String getName() {
1143                return name;
1144        }
1145
1146        public void setName(String name) {
1147                EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
1148                boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
1149                boolean supportSchema = TSQLEnv.supportSchema(vendor);
1150                if (!supportCatalog) {
1151                        assignDatabase(null, null);
1152                } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
1153                        assignDatabase(getDefaultDatabase(), null);
1154                }
1155
1156                if (!supportSchema) {
1157                        assignSchema(null, null);
1158                } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
1159                        assignSchema(getDefaultSchema(), null);
1160                }
1161                
1162                this.name = name;
1163                
1164                updateTableName(supportCatalog, supportSchema);
1165        }
1166
1167        public Pair3<Long, Long, String> getStartPosition() {
1168                return startPosition;
1169        }
1170
1171        public Pair3<Long, Long, String> getEndPosition() {
1172                return endPosition;
1173        }
1174
1175        public List<TableColumn> getColumns() {
1176                return columns;
1177        }
1178
1179        public void addColumn(TableColumn column) {
1180                if (column != null && !this.columns.contains(column)) {
1181                        this.columns.add(column);
1182                }
1183        }
1184
1185        /**
1186         * STRUCT tree: the field path nodes below this table's columns ({@code customer.address},
1187         * {@code customer.address.city}, {@code items[*]}), in the order they were made. They are not columns of their own,
1188         * so they are kept out of {@link #getColumns()}, which star expansion and positional writes walk; the output lists
1189         * them with their columns ({@link #getColumnsWithFieldPaths()}).
1190         */
1191        private List<TableColumn> fieldPathColumns = new ArrayList<TableColumn>();
1192
1193        /** The field path nodes, read-only: they are added by the field-path constructor only, which keeps the index. */
1194        public List<TableColumn> getFieldPathColumns() {
1195                return java.util.Collections.unmodifiableList(fieldPathColumns);
1196        }
1197
1198        /** The field path nodes by their structural identity ({@link FieldNodeKey}), in the order they were made. */
1199        private final java.util.Map<FieldNodeKey, TableColumn> fieldPathIndex =
1200                        new java.util.LinkedHashMap<FieldNodeKey, TableColumn>();
1201
1202        /** Makes {@code column}, a field path node, one of {@link #getFieldPathColumns()} rather than a column. */
1203        void addFieldPathColumn(TableColumn column) {
1204                columns.remove(column);
1205                if (!fieldPathColumns.contains(column)) {
1206                        fieldPathColumns.add(column);
1207                        FieldNodeKey key = FieldNodeKey.of(column);
1208                        if (!fieldPathIndex.containsKey(key)) {
1209                                fieldPathIndex.put(key, column);
1210                        }
1211                }
1212        }
1213
1214        /** Whether this is the relation a table-generating construct (a LATERAL VIEW) adds to one SELECT. */
1215        private boolean arrayRelation;
1216
1217        public boolean isArrayRelation() {
1218                return arrayRelation;
1219        }
1220
1221        public void setArrayRelation(boolean arrayRelation) {
1222                this.arrayRelation = arrayRelation;
1223        }
1224
1225        /** The field path node with identity {@code key}, or null. */
1226        public TableColumn getFieldPathNode(FieldNodeKey key) {
1227                return fieldPathIndex.get(key);
1228        }
1229
1230        /** Plan rule R10: the nested-field declarations of this table, one per CREATE TABLE, in the order made. */
1231        private final List<FieldDeclaration> fieldDeclarations = new ArrayList<FieldDeclaration>(0);
1232
1233        /** The declaration of the CREATE TABLE being analyzed; null between statements. */
1234        private FieldDeclaration buildingFieldDeclaration;
1235
1236        /**
1237         * Starts the declaration of the CREATE TABLE statement {@code created}, which the DROP TABLE {@code dropped}
1238         * ends ({@link FieldDeclaration#NEVER_DROPPED} when none does). Until {@link #endFieldDeclaration()}, the
1239         * declared fields recorded are this declaration's.
1240         */
1241        public FieldDeclaration openFieldDeclaration(long created, long dropped) {
1242                FieldDeclaration declaration = new FieldDeclaration(created, dropped);
1243                fieldDeclarations.add(declaration);
1244                buildingFieldDeclaration = declaration;
1245                return declaration;
1246        }
1247
1248        /** Ends the declaration {@link #openFieldDeclaration} started. */
1249        public void endFieldDeclaration() {
1250                buildingFieldDeclaration = null;
1251        }
1252
1253        /** The declaration declared fields are recorded in: the one being built, else the active one, else a new one. */
1254        FieldDeclaration fieldDeclarationForWrite() {
1255                if (buildingFieldDeclaration != null) {
1256                        return buildingFieldDeclaration;
1257                }
1258                FieldDeclaration active = getActiveFieldDeclaration();
1259                if (active != null) {
1260                        return active;
1261                }
1262                FieldDeclaration declaration = new FieldDeclaration(currentStatementOrdinal(), FieldDeclaration.NEVER_DROPPED);
1263                fieldDeclarations.add(declaration);
1264                return declaration;
1265        }
1266
1267        /** The declaration a read of the statement being analyzed is validated against; null when there is none. */
1268        public FieldDeclaration getActiveFieldDeclaration() {
1269                if (buildingFieldDeclaration != null) {
1270                        return buildingFieldDeclaration;
1271                }
1272                return activeFieldDeclaration(currentStatementOrdinal());
1273        }
1274
1275        private static long currentStatementOrdinal() {
1276                ModelBindingManager manager = ModelBindingManager.get();
1277                return manager == null ? -1 : manager.getCurrentStatementOrdinal();
1278        }
1279
1280        /**
1281         * The declaration in force at the statement {@code at}: the latest one made before it that no DROP ended before
1282         * it. A statement before every CREATE of the table (a script that reads a table before its DDL) and a statement
1283         * after a DROP that no CREATE followed yet use the next declaration; an unknown position uses the latest.
1284         */
1285        FieldDeclaration activeFieldDeclaration(long at) {
1286                if (fieldDeclarations.isEmpty()) {
1287                        return null;
1288                }
1289                if (at < 0) {
1290                        return fieldDeclarations.get(fieldDeclarations.size() - 1);
1291                }
1292                FieldDeclaration inForce = null;
1293                FieldDeclaration next = null;
1294                for (FieldDeclaration declaration : fieldDeclarations) {
1295                        if (declaration.getCreated() <= at) {
1296                                if (at < declaration.getDropped()
1297                                                && (inForce == null || declaration.getCreated() >= inForce.getCreated())) {
1298                                        inForce = declaration;
1299                                }
1300                        } else if (next == null || declaration.getCreated() < next.getCreated()) {
1301                                next = declaration;
1302                        }
1303                }
1304                if (inForce != null) {
1305                        return inForce;
1306                }
1307                return next != null ? next : fieldDeclarations.get(fieldDeclarations.size() - 1);
1308        }
1309
1310        /** The columns as the output lists them: each column followed by the field path nodes below it. */
1311        public List<TableColumn> getColumnsWithFieldPaths() {
1312                if (fieldPathColumns.isEmpty() && loadColumn == null) {
1313                        return columns;
1314                }
1315                List<TableColumn> published = new ArrayList<TableColumn>(columns.size() + fieldPathColumns.size() + 1);
1316                for (TableColumn column : columns) {
1317                        published.add(column);
1318                        for (TableColumn node : fieldPathColumns) {
1319                                if (node.getDeclaredColumn() == column) {
1320                                        published.add(node);
1321                                }
1322                        }
1323                }
1324                for (TableColumn node : fieldPathColumns) {
1325                        if (!published.contains(node)) {
1326                                published.add(node);
1327                        }
1328                }
1329                if (loadColumn != null) {
1330                        published.add(loadColumn);
1331                }
1332                return published;
1333        }
1334
1335        /**
1336         * The {@code *} a bulk load (SQL Server BULK INSERT, Mantis 4962) feeds the table through as a whole, or null.
1337         * Like a field path node it is no column of the table: it is kept out of {@link #getColumns()}, which star
1338         * expansion, positional writes and column lookups walk, so a load changes no other statement's lineage and no
1339         * other statement's star, expansion or pruning reaches the load; the output lists it after the columns.
1340         */
1341        private TableColumn loadColumn;
1342
1343        public TableColumn getLoadColumn() {
1344                return loadColumn;
1345        }
1346
1347        /** Makes {@code column} the table's load column rather than one of its columns. */
1348        public void setLoadColumn(TableColumn column) {
1349                columns.remove(column);
1350                this.loadColumn = column;
1351        }
1352
1353        public TTable getTableObject() {
1354                return tableObject;
1355        }
1356
1357        public String getAlias() {
1358                String aliasString = Arrays.toString(this.alias.toArray(new String[0]));
1359                return aliasString.substring(1, aliasString.length() - 1);
1360        }
1361
1362        public void setAlias(String alias) {
1363                if (!SQLUtil.isEmpty(alias)) {
1364                        this.alias.add(alias);
1365                }
1366        }
1367
1368        public String getFullName() {
1369                return fullName;
1370        }
1371
1372
1373        public boolean hasSubquery() {
1374                return subquery;
1375        }
1376
1377        public String getParent() {
1378                return parent;
1379        }
1380
1381        public void setParent(String parent) {
1382                this.parent = parent;
1383        }
1384
1385//      public TObjectName getTableName() {
1386//              return tableName;
1387//      }
1388
1389        public String getDatabase() {
1390                return database;
1391        }
1392
1393        public String getSchema() {
1394                return schema;
1395        }
1396
1397        public TableRelationRows getRelationRows() {
1398                return relationRows;
1399        }
1400
1401        public boolean isCreateTable() {
1402                return isCreateTable;
1403        }
1404
1405        public void setCreateTable(boolean isCreateTable, boolean fromDDL) {
1406                Stack<TCustomSqlStatement> stmtStack = ModelBindingManager.getGlobalStmtStack();
1407                TCustomSqlStatement stmt = null;
1408                if (stmtStack != null && !stmtStack.isEmpty()) {
1409                        stmt = stmtStack.peek();
1410                }
1411                if (stmt != null && stmt.getClass().getSimpleName().startsWith("TCreate")) {
1412                        Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> position = setPosition(stmt);
1413                        if (position != null) {
1414                                this.startPosition = position.first;
1415                                this.endPosition = position.second;
1416                        }
1417                }
1418                this.isCreateTable = isCreateTable;
1419                if (isCreateTable) {
1420                        for (TableColumn column : columns) {
1421                                column.setShowStar(false);
1422                        }
1423                        setDetermined(true);
1424                        if (fromDDL) {
1425                                setFromDDL(true);
1426                        }
1427                }
1428        }
1429        
1430        public void setCreateTable(boolean isCreateTable) {
1431                setCreateTable(isCreateTable, false);
1432        }
1433
1434        public boolean isView() {
1435                return isView;
1436        }
1437
1438        public void setView(boolean isView) {
1439                this.isView = isView;
1440        }
1441
1442        public boolean isPseudo() {
1443                return isPseudo;
1444        }
1445
1446        public void setPseudo(boolean isPseudo) {
1447                this.isPseudo = isPseudo;
1448        }
1449
1450        public boolean isExternal() {
1451                return isExternal;
1452        }
1453
1454        public void setExternal(boolean isExternal) {
1455                this.isExternal = isExternal;
1456        }
1457
1458        public boolean isStage() {
1459                return isStage;
1460        }
1461
1462        public void setStage(boolean isStage) {
1463                this.isStage = isStage;
1464        }
1465
1466        public boolean isSequence() {
1467                return isSequence;
1468        }
1469
1470        public void setSequence(boolean sequence) {
1471                isSequence = sequence;
1472        }
1473
1474        public boolean isVariable() {
1475                return isVariable;
1476        }
1477
1478        public void setVariable(boolean isVariable) {
1479                this.isVariable = isVariable;
1480        }
1481
1482        public boolean isConstant() {
1483                return isConstant;
1484        }
1485
1486        public void setConstant(boolean isConstant) {
1487                this.isConstant = isConstant;
1488        }
1489
1490        public boolean isTarget() {
1491                return isTarget;
1492        }
1493
1494        public void setTarget(boolean isTarget) {
1495                this.isTarget = isTarget;
1496        }
1497
1498        public String getProcedureId() {
1499                return procedureId;
1500        }
1501
1502        public void setProcedureId(String procedureId) {
1503                this.procedureId = procedureId;
1504        }
1505
1506        public boolean isPath() {
1507                return isPath;
1508        }
1509
1510        public void setPath(boolean isPath) {
1511                setPath(isPath, true);
1512        }
1513
1514        /**
1515         * Marks a file path. With {@code defaultQualifiers} false the analysis defaults (the database and schema a USE or
1516         * an Option sets) stay off it, since a file is no object of a database (SQL Server BULK INSERT, Mantis 4962); the
1517         * file-path database and schema options still apply.
1518         */
1519        public void setPath(boolean isPath, boolean defaultQualifiers) {
1520                this.isPath = isPath;
1521                if(isPath && !defaultQualifiers) {
1522                        String filePathDatabase = ModelBindingManager.getGlobalOption().getFilePathDatabase();
1523                        String filePathSchema = ModelBindingManager.getGlobalOption().getFilePathSchema();
1524                        EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
1525                        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
1526                        boolean supportSchema = TSQLEnv.supportSchema(vendor);
1527                        assignDatabase(supportCatalog && !SQLUtil.isEmpty(filePathDatabase) ? filePathDatabase : null, null);
1528                        assignSchema(supportSchema && !SQLUtil.isEmpty(filePathSchema) ? filePathSchema : null, null);
1529                        // the constructor already folded the defaults into the name; start again from the name as written
1530                        if (this.fullName != null) {
1531                                this.name = this.fullName;
1532                        }
1533                        updateTableName(supportCatalog, supportSchema);
1534                        return;
1535                }
1536                if(isPath) {
1537                        String filePathDatabase = ModelBindingManager.getGlobalOption().getFilePathDatabase();
1538                        String filePathSchema = ModelBindingManager.getGlobalOption().getFilePathSchema();
1539                        if(SQLUtil.isEmpty(filePathDatabase)) {
1540                                assignDatabase(null, null);
1541                        }
1542                        else {
1543                                assignDatabase(filePathDatabase, null);
1544                        }
1545                        if(SQLUtil.isEmpty(filePathSchema)) {
1546                                assignSchema(null, null);
1547                        }
1548                        else {
1549                                assignSchema(filePathSchema, null);
1550                        }
1551
1552                        EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
1553                        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
1554                        boolean supportSchema = TSQLEnv.supportSchema(vendor);
1555                        if (!supportCatalog) {
1556                                assignDatabase(null, null);
1557                        } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
1558                                assignDatabase(getDefaultDatabase(), null);
1559                        }
1560
1561                        if (!supportSchema) {
1562                                assignSchema(null, null);
1563                        } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
1564                                assignSchema(getDefaultSchema(), null);
1565                        }
1566
1567                        updateTableName(supportCatalog, supportSchema);
1568                }
1569        }
1570
1571        public boolean isCursor() {
1572                return isVariable && getSubType() == SubType.cursor;
1573        }
1574
1575        public String getLocation() {
1576                return location;
1577        }
1578
1579        public void setLocation(String location) {
1580                this.location = location;
1581        }
1582
1583        /** The external data source a file path is read through (BULK INSERT ... WITH (DATA_SOURCE = ...)), or null. */
1584        public String getDataSourceName() {
1585                return dataSourceName;
1586        }
1587
1588        public void setDataSourceName(String dataSourceName) {
1589                this.dataSourceName = dataSourceName;
1590        }
1591
1592        /**
1593         * The URI of a file path when it differs from its name: the data source's LOCATION joined with the path the
1594         * statement names; null when the URI is the name.
1595         */
1596        public String getPathUri() {
1597                return pathUri;
1598        }
1599
1600        public void setPathUri(String pathUri) {
1601                this.pathUri = pathUri;
1602        }
1603
1604        public String getFileType() {
1605                return fileType;
1606        }
1607
1608        public void setFileType(String fileType) {
1609                this.fileType = fileType;
1610        }
1611
1612        public String getDisplayName() {
1613                return displayName;
1614        }
1615
1616        public void setDisplayName(String displayName) {
1617                this.displayName = displayName;
1618        }
1619
1620        public String getFileFormat() {
1621                return fileFormat;
1622        }
1623
1624        public void setFileFormat(String fileFormat) {
1625                this.fileFormat = fileFormat;
1626        }
1627
1628        public List<Process> getProcesses() {
1629                return processes;
1630        }
1631
1632        public void addProcess(Process process) {
1633                if (processes == null) {
1634                        processes = new ArrayList<Process>();
1635                }
1636                if (process != null && !processes.contains(process)) {
1637                        processes.add(process);
1638                }
1639        }
1640        
1641        public void removeProcess(Process process) {
1642                if (processes == null) {
1643                        processes = new ArrayList<Process>();
1644                }
1645                if (process != null && processes.contains(process)) {
1646                        processes.remove(process);
1647                }
1648        }
1649
1650        public void setSubType(SubType subType) {
1651                if (this.subType == null || subType == SubType.record_type) {
1652                        this.subType = subType;
1653                }
1654        }
1655
1656        public SubType getSubType() {
1657                return subType;
1658        }
1659
1660        public boolean isStream() {
1661                return isStream;
1662        }
1663
1664        public void setStream(boolean isStream) {
1665                this.isStream = isStream;
1666        }
1667
1668        public boolean isSchema() {
1669                return isSchema;
1670        }
1671
1672        public void setSchema(boolean isSchema) {
1673                this.isSchema = isSchema;
1674        }
1675
1676        public void setDatabase(String database) {
1677                assignDatabase(database, null);
1678        }
1679
1680        public boolean isDatabase() {
1681                return isDatabase;
1682        }
1683
1684        public void setDatabase(boolean isDatabase) {
1685                this.isDatabase = isDatabase;
1686        }
1687
1688        public boolean isDataSource() {
1689                return isDataSource;
1690        }
1691
1692        public void setDataSource(boolean dataSource) {
1693                isDataSource = dataSource;
1694        }
1695
1696        public String getStarStmt() {
1697                return starStmt;
1698        }
1699
1700        public void setStarStmt(String starStmt) {
1701                this.starStmt = starStmt;
1702        }
1703
1704        public void setHasSQLEnv(boolean hasSQLEnv) {
1705                this.hasSQLEnv = hasSQLEnv;
1706        }
1707
1708        public boolean hasSQLEnv() {
1709                return hasSQLEnv;
1710        }
1711
1712        public String getCurrentAlias() {
1713                return currentAlias;
1714        }
1715
1716        public void setCurrentAlias(String currentAlias) {
1717                if (SQLUtil.isEmpty(currentAlias)) {
1718                        this.currentAlias = null;
1719                } else
1720                        this.currentAlias = currentAlias;
1721        }
1722
1723        public Set<String> getAliasList() {
1724                return alias;
1725        }
1726
1727        public String getServer() {
1728                return server;
1729        }
1730
1731        public boolean isDetermined() {
1732                return isDetermined;
1733        }
1734
1735        public void setDetermined(boolean isDetermined) {
1736                this.isDetermined = isDetermined;
1737        }
1738
1739        public boolean isFromDDL() {
1740                return fromDDL;
1741        }
1742
1743        public void setFromDDL(boolean fromDDL) {
1744                this.fromDDL = fromDDL;
1745        }
1746
1747        /**
1748         * Whether this endpoint is created by the analyzed SQL. A parse fact, NOT gated on
1749         * {@link #isDetermined()}; true for CREATE TABLE / SELECT INTO / CTAS / clone
1750         * targets even when the defining query's schema could not be resolved.
1751         */
1752        public boolean isCreatedInSql() {
1753                return createdInSql;
1754        }
1755
1756        public void setCreatedInSql(boolean createdInSql) {
1757                this.createdInSql = createdInSql;
1758        }
1759
1760        public EndpointIntroduction getEndpointIntroduction() {
1761                return endpointIntroduction;
1762        }
1763
1764        /**
1765         * Records how the endpoint was introduced. First non-UNKNOWN writer wins, so the
1766         * structural creation site (set early at build time) is not later clobbered.
1767         */
1768        public void setEndpointIntroduction(EndpointIntroduction endpointIntroduction) {
1769                if (endpointIntroduction == null) {
1770                        return;
1771                }
1772                if (this.endpointIntroduction == null || this.endpointIntroduction == EndpointIntroduction.UNKNOWN) {
1773                        this.endpointIntroduction = endpointIntroduction;
1774                }
1775        }
1776
1777        /**
1778         * Existing schema/model resolution state, exposed under the endpoint-classification
1779         * name. Mirrors {@link #isDetermined()} exactly; introduces no new resolution logic.
1780         */
1781        public boolean isResolved() {
1782                return isDetermined;
1783        }
1784
1785        /**
1786         * Delimiter-normalized, dialect-aware form of {@link #getName()} (brackets / quotes
1787         * stripped per identifier segment). Computed on access from the current name; does
1788         * NOT mutate {@link #getName()}, which stays raw. Returns the raw name when no
1789         * normalization is possible (e.g. no global vendor context).
1790         */
1791        public String getNormalizedName() {
1792                if (name == null) {
1793                        return null;
1794                }
1795                try {
1796                        String normalized = gudusoft.gsqlparser.dlineage.util.LegacyDlineageWireNamesV1.tableName(name);
1797                        return normalized == null ? name : normalized;
1798                } catch (Exception e) {
1799                        return name;
1800                }
1801        }
1802
1803        /**
1804         * Records that this endpoint's identifier was assembled at runtime by a dynamic-SQL
1805         * fold. {@code parts} is the outermost-first, pipe-joined set of affected name
1806         * segments; {@code identityTemplated} says the hole reaches the leading character of
1807         * the object-name segment (so a {@code #}/{@code @} prefix there is fold text, not a
1808         * real temp / table-variable marker). Additive across repeated marks: parts union,
1809         * identity ORs — one folded statement can reach the same endpoint twice.
1810         *
1811         * <p>Callers MUST only invoke this from inside a dynamic-fold context; see
1812         * {@link EndpointKind#DYNAMIC_TEMPLATE}.
1813         */
1814        public void markDynamicTemplate(String parts, boolean identityTemplated) {
1815                if (parts != null && parts.length() > 0) {
1816                        if (this.templatedParts == null) {
1817                                this.templatedParts = parts;
1818                        } else if (!this.templatedParts.equals(parts)) {
1819                                java.util.LinkedHashSet<String> merged = new java.util.LinkedHashSet<String>();
1820                                for (String part : TEMPLATED_PART_ORDER) {
1821                                        if (containsPart(this.templatedParts, part) || containsPart(parts, part)) {
1822                                                merged.add(part);
1823                                        }
1824                                }
1825                                StringBuilder sb = new StringBuilder();
1826                                for (String part : merged) {
1827                                        if (sb.length() > 0) {
1828                                                sb.append('|');
1829                                        }
1830                                        sb.append(part);
1831                                }
1832                                this.templatedParts = sb.toString();
1833                        }
1834                }
1835                this.templatedIdentity = this.templatedIdentity || identityTemplated;
1836        }
1837
1838        /** Canonical outermost-first ordering of the segment labels used by {@link #getTemplatedParts()}. */
1839        private static final String[] TEMPLATED_PART_ORDER = { "server", "db", "schema", "name" };
1840
1841        private static boolean containsPart(String parts, String part) {
1842                if (parts == null) {
1843                        return false;
1844                }
1845                for (String candidate : parts.split("\\|")) {
1846                        // non-identifier-compare: fixed internal label vocabulary, not a DB object name
1847                        if (candidate.equals(part)) {
1848                                return true;
1849                        }
1850                }
1851                return false;
1852        }
1853
1854        /**
1855         * Which identifier segments of {@link #getName()} were assembled at runtime, pipe-joined
1856         * outermost-first ({@code "db"}, {@code "db|name"}, ...); {@code null} when this endpoint
1857         * is not runtime-templated. Lets a consumer match a templated skeleton against a
1858         * later-resolved concrete name without re-parsing the name text.
1859         */
1860        public String getTemplatedParts() {
1861                return templatedParts;
1862        }
1863
1864        /** True when this endpoint's identifier was assembled at runtime by a dynamic-SQL fold. */
1865        public boolean isDynamicTemplate() {
1866                return templatedParts != null;
1867        }
1868
1869        /**
1870         * Withdraws a dynamic-template mark. Used when the SAME node turns out to also be
1871         * referenced by a STATIC statement (by-name model reuse): the static reference provably
1872         * names a real object, so publishing the template claim on the shared node would make
1873         * that reference wrong — see the shared-node contract in
1874         * docs/tmp/dynamic-template-endpoint-provenance.md.
1875         */
1876        public void clearDynamicTemplate() {
1877                this.templatedParts = null;
1878                this.templatedIdentity = false;
1879        }
1880
1881        /**
1882         * Authoritative {@link EndpointKind}, computed on access from the current (raw)
1883         * name and the active dialect. Quote-robust: each identifier segment is delimiter-
1884         * stripped before the {@code #}/{@code ##}/{@code @}/{@code tempdb} tests. This
1885         * reads only the new classification; it never inspects or mutates {@link #subType}.
1886         *
1887         * <p>§5.6 minimum scope: distinguishes T-SQL temp / global-temp / table-variable /
1888         * tempdb objects from plain catalog objects. CTE / derived / TVF / constant kinds
1889         * are deferred and reported as {@link EndpointKind#UNKNOWN} here.
1890         */
1891        public static boolean isTsqlFamilyVendor(EDbVendor vendor) {
1892                return vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql
1893                                || vendor == EDbVendor.dbvsybase || vendor == EDbVendor.dbvsybasease
1894                                || vendor == EDbVendor.dbvsqlanywhere || vendor == EDbVendor.dbvsybaseiq;
1895        }
1896
1897        public EndpointKind getEndpointKind() {
1898                if (SQLUtil.isEmpty(name)) {
1899                        return EndpointKind.UNKNOWN;
1900                }
1901
1902                EDbVendor vendor = null;
1903                if (ModelBindingManager.getGlobalOption() != null) {
1904                        vendor = ModelBindingManager.getGlobalOption().getVendor();
1905                }
1906
1907                if (isTsqlFamilyVendor(vendor)) {
1908                        List<String> segments = SQLUtil.parseNames(name);
1909                        if (!segments.isEmpty()) {
1910                                String last = SQLUtil.trimColumnStringQuote(segments.get(segments.size() - 1).trim());
1911                                // A '#'/'##'/'@' prefix that came out of a dynamic fold hole is runtime text,
1912                                // not evidence of a temp table / table variable: 'INSERT INTO ' + QUOTENAME(@tab)
1913                                // folds to a table literally named @tab, which is a runtime-built CATALOG name.
1914                                // Only then does DYNAMIC_TEMPLATE displace the temp family; a genuinely literal
1915                                // '#tmp_' prefix keeps its temp kind (the templated segments stay readable via
1916                                // getTemplatedParts()), because losing TEMP would send a consumer to the catalog.
1917                                if (last.startsWith("##")) {
1918                                        return templatedIdentity ? EndpointKind.DYNAMIC_TEMPLATE : EndpointKind.GLOBAL_TEMP_TABLE;
1919                                }
1920                                if (last.startsWith("#")) {
1921                                        return templatedIdentity ? EndpointKind.DYNAMIC_TEMPLATE : EndpointKind.LOCAL_TEMP_TABLE;
1922                                }
1923                                if (last.startsWith("@")) {
1924                                        return templatedIdentity ? EndpointKind.DYNAMIC_TEMPLATE : EndpointKind.TABLE_VARIABLE;
1925                                }
1926                                String first = SQLUtil.trimColumnStringQuote(segments.get(0).trim());
1927                                if (segments.size() > 1 && "tempdb".equalsIgnoreCase(first)) {
1928                                        // A literal leading "tempdb" proves the object lives in tempdb whatever the
1929                                        // later segments template to, so the kind stands and templatedParts carries
1930                                        // the rest of the story.
1931                                        return EndpointKind.TEMPDB_OBJECT;
1932                                }
1933                        }
1934                }
1935
1936                // A runtime-assembled identifier is never a catalog object name: report the
1937                // template, so a consumer does not bind it (or, worse, match an unrelated object
1938                // that happens to share the folded spelling). Marked only inside a fold context.
1939                if (templatedParts != null) {
1940                        return EndpointKind.DYNAMIC_TEMPLATE;
1941                }
1942
1943                // Only a plain real table is a catalog object here. Resultsets / derived /
1944                // views / variables / stages etc. are deferred kinds — leave them UNKNOWN
1945                // rather than mislabel them CATALOG_OBJECT.
1946                if (!subquery && !isView && !isPseudo && !isStage && !isSequence && !isStream
1947                                && !isVariable && !isConstant && !isDatabase && !isSchema && !isDataSource
1948                                && subType != SubType.alias_table && subType != SubType.values_table
1949                                && subType != SubType.synonym && subType != SubType.function
1950                                && subType != SubType.unnest) {
1951                        return EndpointKind.CATALOG_OBJECT;
1952                }
1953
1954                return EndpointKind.UNKNOWN;
1955        }
1956
1957        private Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> setPositionInternal(TSourceToken startToken, TSourceToken endToken) {
1958                if (startToken == null || endToken == null) {
1959                        return null;
1960                }
1961
1962                Pair3<Long, Long, String> startPosition = new Pair3<Long, Long, String>(startToken.lineNo, startToken.columnNo,
1963                                ModelBindingManager.getGlobalHash());
1964                Pair3<Long, Long, String> endPosition = new Pair3<Long, Long, String>(endToken.lineNo, endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(),
1965                                ModelBindingManager.getGlobalHash());
1966
1967                if(this.startPosition == null && this.endPosition == null) {
1968                        this.startPosition = startPosition;
1969                        this.endPosition = endPosition;
1970                }
1971
1972                boolean traceTablePosition = ModelBindingManager.getGlobalOption().isTraceTablePosition();
1973
1974                
1975                Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> position = Pair.create(startPosition, endPosition);
1976                
1977                if (traceTablePosition) {
1978                        positions.add(position);
1979                }
1980                
1981                return position;
1982        }
1983
1984        public Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> setPosition(TTable table) {
1985                if (table == null) {
1986                        return null;
1987                }
1988                
1989                TSourceToken startToken = table.getStartToken();
1990                TSourceToken endToken = table.getEndToken();
1991                if (startToken == null && table.getSubquery() != null) {
1992                        startToken = table.getSubquery().getStartToken();
1993                        endToken = table.getSubquery().getEndToken();
1994                }
1995
1996                return setPositionInternal(startToken, endToken);
1997        }
1998
1999        public Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> setPosition(TObjectName tableName) {
2000                if (tableName == null) {
2001                        return null;
2002                }
2003                
2004                TSourceToken startToken = tableName.getStartToken();
2005                TSourceToken endToken = tableName.getEndToken();
2006                
2007                return setPositionInternal(startToken, endToken);
2008        }
2009
2010        public Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>> setPosition(TCustomSqlStatement stmt) {
2011                if (stmt == null) {
2012                        return null;
2013                }
2014                
2015                TSourceToken startToken = stmt.getStartToken();
2016                TSourceToken endToken = stmt.getEndToken();
2017                
2018                return setPositionInternal(startToken, endToken);
2019        }
2020
2021        public Set<Pair<Pair3<Long, Long, String>, Pair3<Long, Long, String>>> getPositions() {
2022                return positions;
2023        }
2024        
2025        public TableColumn findColumn(String columnName) {
2026                if (columns == null) {
2027                        return null;
2028                }
2029                for (TableColumn column : columns) {
2030                        if (DlineageUtil.compareColumnIdentifier(column.getName(), columnName)) {
2031                                return column;
2032                        }
2033                }
2034                return null;
2035        }
2036
2037        public void removeColumn(TableColumn column) {
2038                if (columns != null && columns.contains(column)) {
2039                        columns.remove(column);
2040                }
2041        }
2042
2043        public void removeColumn(String columnName) {
2044                TableColumn column = findColumn(columnName);
2045                if (column != null) {
2046                        removeColumn(column);
2047                }
2048        }
2049}