001
002package gudusoft.gsqlparser.dlineage.dataflow.model;
003
004import gudusoft.gsqlparser.dlineage.routine.RoutineAliasKey;
005import gudusoft.gsqlparser.dlineage.routine.RoutineAliasRegistry;
006import gudusoft.gsqlparser.EDbVendor;
007import gudusoft.gsqlparser.resolver2.metadata.StatementMetadata.ProcedureEnvironment;
008import gudusoft.gsqlparser.EExpressionType;
009import gudusoft.gsqlparser.ESetOperatorType;
010import gudusoft.gsqlparser.ETableEffectType;
011import gudusoft.gsqlparser.TCustomSqlStatement;
012import gudusoft.gsqlparser.dlineage.util.DlineageUtil;
013import gudusoft.gsqlparser.dlineage.util.NormalNameCollisions;
014import gudusoft.gsqlparser.dlineage.util.Pair;
015import gudusoft.gsqlparser.dlineage.util.Pair3;
016import gudusoft.gsqlparser.dlineage.util.SHA256;
017import gudusoft.gsqlparser.dlineage.util.ScopedNameKey;
018import gudusoft.gsqlparser.dlineage.util.TableIdentityKey;
019import gudusoft.gsqlparser.nodes.*;
020import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
021import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
022import gudusoft.gsqlparser.sqlenv.QualifiedCanonKey;
023import gudusoft.gsqlparser.sqlenv.TSQLEnv;
024import gudusoft.gsqlparser.sqlenv.TSQLTable;
025import gudusoft.gsqlparser.stmt.*;
026import gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare;
027import gudusoft.gsqlparser.util.IndexedLinkedHashMap;
028import gudusoft.gsqlparser.util.Logger;
029import gudusoft.gsqlparser.util.LoggerFactory;
030import gudusoft.gsqlparser.util.SQLUtil;
031
032import java.util.*;
033
034@SuppressWarnings({ "unchecked", "rawtypes" })
035public class ModelBindingManager {
036
037        private static final Logger logger = LoggerFactory.getLogger(ModelBindingManager.class);
038
039        private final Map modelBindingMap = new LinkedHashMap();
040        private final Map processModelBindingMap = new LinkedHashMap();
041        private final Map viewModelBindingMap = new LinkedHashMap();
042        private final Map insertModelBindingMap = new LinkedHashMap();
043        private final Map createModelBindingMap = new LinkedHashMap();
044        private final Map createModelQuickBindingMap = new LinkedHashMap();
045        private final Map mergeModelBindingMap = new LinkedHashMap();
046        private final Map updateModelBindingMap = new LinkedHashMap();
047        private final Map cursorModelBindingMap = new LinkedHashMap();
048        private final Map tableNamesMap = new LinkedHashMap();
049        // Models bound by table identity: a canonical miss never falls back to their folded name.
050        private final Set<Table> identityBoundTables = Collections.newSetFromMap(new IdentityHashMap<Table, Boolean>());
051        // Legacy normal names that different names share in this analysis; see normalName.
052        private EDbVendor contestedVendor;
053        private Set<String> contestedNormalNames = Collections.emptySet();
054        private Set<String> contestedLegacyNames = Collections.emptySet();
055        private final Map<String, String> collisionFreeNames = new HashMap<String, String>();
056        private boolean legacyNormalNames;
057        // Metadata identity lets a proven caller/callee share one endpoint after name scopes close.
058        private final Map<TSQLTable, Table> temporaryMetadataModels = new IdentityHashMap<>();
059        private final Map<TSQLTable, TStoredProcedureSqlStatement> temporaryMetadataOwners = new IdentityHashMap<>();
060        /** The scopes of the routine bodies being analyzed, innermost first; see enterTemporaryTableScope. */
061        private final Deque<List<Map>> temporaryScopes = new ArrayDeque<List<Map>>();
062        private final Set<Table> dropTables = new HashSet<Table>();
063        // The routine model bound to each routine statement. The same statement is bound to other models later (a SQL
064        // function's result set), which replace it in modelBindingMap; this keeps the routine's own.
065        private final Map<TCustomSqlStatement, Procedure> routineModels = new IdentityHashMap<TCustomSqlStatement, Procedure>();
066        private gudusoft.gsqlparser.dlineage.routine.RoutineShadow.State routineShadowState;
067        // Routines and Oracle packages by the names as written (RoutineAliasRegistry keeps the legacy index's precedence).
068        private final RoutineAliasRegistry<Procedure> procedureAliases = new RoutineAliasRegistry<Procedure>();
069        private final RoutineAliasRegistry<OraclePackage> packageAliases = new RoutineAliasRegistry<OraclePackage>();
070        // A routine's function results (the result sets of its body) by the routine's name as written.
071        private final RoutineAliasRegistry<Set<Object>> functionTableAliases = new RoutineAliasRegistry<Set<Object>>();
072        // The deprecated text-keyed API's own maps: exactly the entries bound through it, keyed by the text as given and
073        // looked up as the legacy maps were, with no analysis context needed (see bindProcedureByName).
074        private final Map<String, Procedure> legacyProcedureNames = new LinkedHashMap<String, Procedure>();
075        private final Map<String, OraclePackage> legacyPackageNames = new LinkedHashMap<String, OraclePackage>();
076        private final Map<Object, Set<Object>> legacyFunctionTables = new LinkedHashMap<Object, Set<Object>>();
077        // Local symbols (variables, cursors, declared types, trigger correlation names, table variables) by scope and name
078        // as written.
079        private final gudusoft.gsqlparser.dlineage.symbol.LocalSymbolRegistry localSymbols =
080                        new gudusoft.gsqlparser.dlineage.symbol.LocalSymbolRegistry();
081        // The cursor models by cursor name as written, in the declaring scope.
082        private final gudusoft.gsqlparser.dlineage.symbol.LocalSymbolRegistry localCursorModels =
083                        new gudusoft.gsqlparser.dlineage.symbol.LocalSymbolRegistry();
084        // The identifier rules the analysis keyed this manager's routines under, so the deprecated text API can read a
085        // name the same way from a thread with no analysis on it.
086        private gudusoft.gsqlparser.sqlenv.IdentifierService routineService;
087        private final Set<Relationship> relationHolder = Collections.synchronizedSet(new LinkedHashSet<Relationship>());
088        // A table alias is resolved inside one statement: keyed by the statement's hash and the alias's
089        // identity as written, never by a folded string.
090        private final Map<ScopedNameKey, TTable> tableAliasMap = new LinkedHashMap<ScopedNameKey, TTable>();
091        private final Map<TCustomSqlStatement, String> tableHashMap = new LinkedHashMap();
092        private final Map<TCustomSqlStatement, String> topStmtHashMap = new HashMap();
093        private final Map hashSQLMap = new HashMap();
094        private final Map dynamicSQLMap = new HashMap();
095        private final Set tableSet = new LinkedHashSet();
096        private final Set resultSetSet = new LinkedHashSet();
097        private final Set<String> dblinkTableSet = new LinkedHashSet();
098
099        // Pipelined function stitching state
100        // Keyed by a type's name as written under the analysis's rules (PipelinedFunctionAnalyzer); the deprecated
101        // String API keeps its own text keys in the same maps.
102        private final Map<Object, List<String>> objectTypeIndex = new LinkedHashMap<>();
103        private final Map<Object, String> collectionToElementTypeIndex = new LinkedHashMap<>();
104        private final Map<Object, List<PipelinedFunctionSignature>> pipelinedSignatures = new LinkedHashMap<>();
105        private final List<PendingPipelinedCallSite> pendingPipelinedCallSites = new ArrayList<>();
106        private final Set<String> resolvingPipelinedFunctions = new HashSet<>();
107
108        public Map<String, Long> DISPLAY_ID = new LinkedHashMap<String, Long>();
109        public Map<Long, String> DISPLAY_NAME = new LinkedHashMap<Long, String>();
110        public Map<String, String> virtualTableNames = new LinkedHashMap<String, String>();
111
112        public long TABLE_COLUMN_ID = 0;
113        public long RELATION_ID = 0;
114        public int virtualTableIndex = -1;
115
116        /** The SQL-LR observation recorder of this analysis, or null (plan 06; internal). */
117        private SqllrObservations sqllrObservations;
118
119        /** The SQL-LR observation recorder of this analysis, or null. Internal API of the SQL-LR producer. */
120        public SqllrObservations getSqllrObservations() {
121                return sqllrObservations;
122        }
123
124        /** Binds a SQL-LR observation recorder to this analysis. Internal API of the SQL-LR producer. */
125        public void setSqllrObservations(SqllrObservations observations) {
126                this.sqllrObservations = observations;
127        }
128
129        /**
130         * The position in the script of the top-level statement being analyzed, as ordered by the analyzer: the unit,
131         * then the statement in the unit; -1 when unknown. A read of a nested field is validated against the CREATE
132         * TABLE in force at this position (plan rule R10, {@link Table#getActiveFieldDeclaration()}).
133         */
134        private long currentStatementOrdinal = -1;
135
136        public long getCurrentStatementOrdinal() {
137                return currentStatementOrdinal;
138        }
139
140        public void setCurrentStatementOrdinal(long currentStatementOrdinal) {
141                this.currentStatementOrdinal = currentStatementOrdinal;
142        }
143
144        private final static ThreadLocal localManager = new ThreadLocal();
145        private final static ThreadLocal globalDatabase = new ThreadLocal();
146        private final static ThreadLocal globalSchema = new ThreadLocal();
147        /**
148         * The form the global database / schema text arrived in: absent for a SQL spelling (a {@code USE},
149         * a session default, a search path), {@code CATALOG_STORED} for the stored name a metadata export
150         * gives a query's group. The text stays the bare name, for display; lineage keys read it by its form.
151         */
152        private final static ThreadLocal<IdentifierInputForm> globalDatabaseForm = new ThreadLocal<IdentifierInputForm>();
153        private final static ThreadLocal<IdentifierInputForm> globalSchemaForm = new ThreadLocal<IdentifierInputForm>();
154        private final static ThreadLocal schemaUnsetStatements = new ThreadLocal();
155        private final static ThreadLocal globalVendor = new ThreadLocal();
156        private final static ThreadLocal globalSQLEnv = new ThreadLocal();
157        private final static ThreadLocal globalHash = new ThreadLocal();
158        private final static ThreadLocal globalStmtStack = new ThreadLocal();
159        /**
160         * Set while the statement on {@link #getGlobalStmtStack()} is a stand-in rather than the
161         * statement the work belongs to — pipelined stitching runs after every pass and pushes the
162         * script's LAST statement just to have one. Anything that reads the statement for provenance,
163         * such as a comment, must not attribute that statement's text to these relationships.
164         */
165        private final static ThreadLocal syntheticStmtContext = new ThreadLocal();
166
167        public static boolean isSyntheticStmtContext() {
168                return Boolean.TRUE.equals(syntheticStmtContext.get());
169        }
170
171        public static void setSyntheticStmtContext(boolean synthetic) {
172                syntheticStmtContext.set(Boolean.valueOf(synthetic));
173        }
174        private final static ThreadLocal globalSqlInfo = new ThreadLocal();
175        private final static ThreadLocal globalOption = new ThreadLocal();
176        private final static ThreadLocal globalOraclePackage = new ThreadLocal();
177        private final static ThreadLocal globalProcedure = new ThreadLocal();
178
179        public static void set(ModelBindingManager modelManager) {
180                if (modelManager != null) {
181                        localManager.set(modelManager);
182                }
183        }
184
185        public static ModelBindingManager get() {
186                return (ModelBindingManager) localManager.get();
187        }
188
189        public static void setGlobalDatabase(String database) {
190                if (database != null) {
191                        globalDatabase.set(database);
192                        globalDatabaseForm.remove();
193                }
194        }
195
196        /** {@link #setGlobalDatabase(String)} with the form the text arrived in; null is a SQL spelling. */
197        public static void setGlobalDatabase(String database, IdentifierInputForm form) {
198                if (database != null) {
199                        globalDatabase.set(database);
200                        if (form == IdentifierInputForm.CATALOG_STORED) {
201                                globalDatabaseForm.set(form);
202                        } else {
203                                globalDatabaseForm.remove();
204                        }
205                }
206        }
207
208        /** {@code CATALOG_STORED} when the global database text is a stored name, else null. */
209        public static IdentifierInputForm getGlobalDatabaseForm() {
210                return globalDatabaseForm.get();
211        }
212
213        /** The global database as a SQL spelling: its text, delimited when it is a stored name that needs it. */
214        public static String getGlobalDatabaseSpelling() {
215                String database = getGlobalDatabase();
216                return getGlobalDatabaseForm() == IdentifierInputForm.CATALOG_STORED
217                                ? DlineageUtil.storedNameSpelling(ESQLDataObjectType.dotCatalog, database) : database;
218        }
219
220        public static String getGlobalDatabase() {
221                return (String) globalDatabase.get();
222        }
223
224        public static void removeGlobalDatabase() {
225                globalDatabase.remove();
226                globalDatabaseForm.remove();
227        }
228
229        public static void setGlobalSchema(String schema) {
230                if (schema != null) {
231                        globalSchema.set(schema);
232                        globalSchemaForm.remove();
233                }
234        }
235
236        /** {@link #setGlobalSchema(String)} with the form the text arrived in; null is a SQL spelling. */
237        public static void setGlobalSchema(String schema, IdentifierInputForm form) {
238                if (schema != null) {
239                        globalSchema.set(schema);
240                        if (form == IdentifierInputForm.CATALOG_STORED) {
241                                globalSchemaForm.set(form);
242                        } else {
243                                globalSchemaForm.remove();
244                        }
245                }
246        }
247
248        /** {@code CATALOG_STORED} when the global schema text is a stored name, else null. */
249        public static IdentifierInputForm getGlobalSchemaForm() {
250                return globalSchemaForm.get();
251        }
252
253        /** The global schema as a SQL spelling: its text, delimited when it is a stored name that needs it. */
254        public static String getGlobalSchemaSpelling() {
255                String schema = getGlobalSchema();
256                return getGlobalSchemaForm() == IdentifierInputForm.CATALOG_STORED
257                                ? DlineageUtil.storedNameSpelling(ESQLDataObjectType.dotSchema, schema) : schema;
258        }
259
260        public static void removeGlobalSchema() {
261                globalSchema.remove();
262                globalSchemaForm.remove();
263        }
264
265        public static String getGlobalSchema() {
266                return (String) globalSchema.get();
267        }
268
269        /**
270         * The statements that a Snowflake {@code USE <database>} left with NO current
271         * schema, i.e. everything after such a switch up to the next schema switch.
272         *
273         * <p>Recorded as a set of statements rather than as a running flag on
274         * purpose. The analyzer walks the statement list several times - one pass for
275         * CREATE TABLE, another for views, another for procedures - replaying the
276         * {@code USE} statements in each. A running flag set during one pass is still
277         * set when a LATER pass reaches a statement that precedes the switch, so it
278         * would apply the clearing backwards and strip an already-exact qualification
279         * off a statement that ran while the old schema was still current. Keyed by
280         * statement, the answer is the same no matter which pass asks.
281         *
282         * <p>See GitHub #715 / MantisBT 4677.
283         */
284        public static void setSchemaUnsetStatements(Set<TCustomSqlStatement> statements) {
285                if (statements == null || statements.isEmpty()) {
286                        schemaUnsetStatements.remove();
287                } else {
288                        schemaUnsetStatements.set(statements);
289                }
290        }
291
292        public static void removeSchemaUnsetStatements() {
293                schemaUnsetStatements.remove();
294        }
295
296        /**
297         * Whether the statement being analyzed right now has no current schema
298         * because a Snowflake {@code USE <database>} unset it.
299         *
300         * @see #setSchemaUnsetStatements(Set)
301         */
302        @SuppressWarnings("unchecked")
303        public static boolean isGlobalSchemaUnset() {
304                Set<TCustomSqlStatement> unset = (Set<TCustomSqlStatement>) schemaUnsetStatements.get();
305                if (unset == null || unset.isEmpty()) {
306                        return false;
307                }
308                Stack<TCustomSqlStatement> stack = getGlobalStmtStack();
309                if (stack == null || stack.isEmpty()) {
310                        return false;
311                }
312                return unset.contains(DlineageUtil.getTopStmt(stack.peek()));
313        }
314
315        public static void setGlobalHash(String hash) {
316                if (hash != null) {
317                        globalHash.set(hash);
318                }
319        }
320
321        public static void removeGlobalHash() {
322                globalHash.remove();
323        }
324
325        public static String getGlobalHash() {
326                return (String) globalHash.get();
327        }
328
329        public static void setGlobalOraclePackage(OraclePackage currentOraclePackage) {
330                if (currentOraclePackage != null) {
331                        globalOraclePackage.set(currentOraclePackage);
332                }
333        }
334
335        public static void removeGlobalOraclePackage() {
336                globalOraclePackage.remove();
337        }
338
339        public static OraclePackage getGlobalOraclePackage() {
340                return (OraclePackage) globalOraclePackage.get();
341        }
342        
343        public static void setGlobalProcedure(TStoredProcedureSqlStatement procedureSqlStatement) {
344                if (procedureSqlStatement != null) {
345                        globalProcedure.set(procedureSqlStatement);
346                }
347        }
348
349        public static void removeGlobalProcedure() {
350                globalProcedure.remove();
351                ModelBindingManager.get().cleanTempTable();
352        }
353
354        /**
355         * Puts back the routine a statement was analyzed under, null for none. Unlike {@link #removeGlobalProcedure()}
356         * this ends no routine: the temporary tables stay where they are.
357         */
358        public static void restoreGlobalProcedure(TStoredProcedureSqlStatement procedureSqlStatement) {
359                if (procedureSqlStatement != null) {
360                        globalProcedure.set(procedureSqlStatement);
361                } else {
362                        globalProcedure.remove();
363                }
364        }
365
366        public static TStoredProcedureSqlStatement getGlobalProcedure() {
367                return (TStoredProcedureSqlStatement) globalProcedure.get();
368        }
369        
370        public static void setGlobalVendor(EDbVendor vendor) {
371                if (vendor != null) {
372                        globalVendor.set(vendor);
373                }
374        }
375
376        public static void removeGlobalVendor() {
377                globalVendor.remove();
378        }
379
380        public static EDbVendor getGlobalVendor() {
381                return (EDbVendor) globalVendor.get();
382        }
383
384        /**
385         * The frozen {@link gudusoft.gsqlparser.sqlenv.IdentifierService} of the analysis running on
386         * this thread — one engine, configured once at the analyzer entry, used by every lineage
387         * identity key, binding map and output merge in the run.
388         *
389         * <p>Carried the same way this class already carries the vendor, the SQL environment and the
390         * option, so that two analyses with opposite configurations can run concurrently without
391         * either seeing the other's rules. It is NOT consulted by the vendor-only static comparison
392         * API in {@code SQLUtil} / {@code IdentifierService}: those keep their stated default
393         * semantics for independent callers.
394         */
395        private final static ThreadLocal globalIdentifierService = new ThreadLocal();
396
397        /**
398         * Install the analysis's identifier service for this thread.
399         *
400         * <p>Callers save the previous value and restore it in a {@code finally} block; nesting on one
401         * thread and thread-pool reuse both depend on that, and a leaked service would let the next
402         * analysis on the same worker key its model under another run's rules.
403         *
404         * @param service the frozen service; null clears the binding
405         */
406        public static void setGlobalIdentifierService(gudusoft.gsqlparser.sqlenv.IdentifierService service) {
407                if (service == null) {
408                        globalIdentifierService.remove();
409                } else {
410                        globalIdentifierService.set(service);
411                }
412        }
413
414        public static void removeGlobalIdentifierService() {
415                globalIdentifierService.remove();
416        }
417
418        /**
419         * The service bound to this thread with no vendor-default substitution — what
420         * {@link #setGlobalIdentifierService} has to be handed back to restore the previous state.
421         *
422         * @return the bound service, or null when nothing is bound
423         */
424        /**
425         * Publishes the legacy normal names that different names share in this analysis, as
426         * {@link NormalNameCollisions#scan} found them; {@code null} clears them.
427         */
428        public void setContestedNormalNames(EDbVendor vendor, Set<String> contested) {
429                boolean none = vendor == null || contested == null || contested.isEmpty();
430                contestedVendor = none ? null : vendor;
431                contestedNormalNames = none ? Collections.<String>emptySet() : contested;
432                contestedLegacyNames = none ? Collections.<String>emptySet() : NormalNameCollisions.legacyNames(contested);
433                collisionFreeNames.clear();
434        }
435
436        /**
437         * Makes {@link #normalName} return legacy names, for a pass that only relabels finished output
438         * (normalized display names are bare, as the legacy fold made them).
439         *
440         * @return the previous setting, to restore
441         */
442        public boolean useLegacyNormalNames(boolean legacy) {
443                boolean previous = legacyNormalNames;
444                legacyNormalNames = legacy;
445                return previous;
446        }
447
448        /** Whether any two different names in this analysis share a legacy normal name. */
449        public boolean hasContestedNormalNames() {
450                return !contestedLegacyNames.isEmpty();
451        }
452
453        /**
454         * The normal name of one identifier segment in this analysis: its legacy name, unless a
455         * different name shares that string (see {@link NormalNameCollisions}).
456         */
457        public String normalName(EDbVendor vendor, ESQLDataObjectType type, String origin, String legacy) {
458                if (contestedLegacyNames.isEmpty() || legacyNormalNames || legacy == null
459                                || vendor != contestedVendor || type == null || origin == null
460                                || !contestedLegacyNames.contains(legacy.toUpperCase(Locale.ROOT))) {
461                        return legacy;
462                }
463                String memoKey = type.name() + '\u0000' + origin;
464                String name = collisionFreeNames.get(memoKey);
465                if (name == null) {
466                        name = NormalNameCollisions.normalName(contestedNormalNames, identifierServiceFor(vendor), type,
467                                        origin, legacy);
468                        collisionFreeNames.put(memoKey, name);
469                }
470                return name;
471        }
472
473        /** The analysis's own identifier rules when they are for {@code vendor}, else the vendor's. */
474        private static gudusoft.gsqlparser.sqlenv.IdentifierService identifierServiceFor(EDbVendor vendor) {
475                gudusoft.gsqlparser.sqlenv.IdentifierService service = getGlobalIdentifierService();
476                return service != null && service.getProfile().getVendor() == vendor ? service
477                                : gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor);
478        }
479
480        public static gudusoft.gsqlparser.sqlenv.IdentifierService getGlobalIdentifierServiceRaw() {
481                return (gudusoft.gsqlparser.sqlenv.IdentifierService) globalIdentifierService.get();
482        }
483
484        /**
485         * The analysis's identifier service, or the vendor default when no analysis configured one.
486         *
487         * @return the service, or null when not even a vendor is known on this thread
488         */
489        public static gudusoft.gsqlparser.sqlenv.IdentifierService getGlobalIdentifierService() {
490                gudusoft.gsqlparser.sqlenv.IdentifierService service =
491                        (gudusoft.gsqlparser.sqlenv.IdentifierService) globalIdentifierService.get();
492                if (service != null) {
493                        return service;
494                }
495                EDbVendor vendor = getGlobalVendor();
496                return vendor == null ? null
497                        : gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor);
498        }
499
500        public static void setGlobalSQLEnv(TSQLEnv sqlenv) {
501                if (sqlenv != null) {
502                        globalSQLEnv.set(sqlenv);
503                }
504        }
505
506        public static void removeGlobalSQLEnv() {
507                globalSQLEnv.remove();
508        }
509
510        public static TSQLEnv getGlobalSQLEnv() {
511                return (TSQLEnv) globalSQLEnv.get();
512        }
513
514        public static String getGlobalServer() {
515                TSQLEnv sqlEnv = getGlobalSQLEnv();
516                if (sqlEnv != null)
517                        return TSQLEnv.DEFAULT_SERVER_NAME.equals(sqlEnv.getDefaultServerName()) ? null : sqlEnv.getDefaultServerName();
518                return null;
519        }
520
521        /**
522         * Resolve the effective default database (catalog) used to qualify
523         * unqualified / partial object names, with a three-tier precedence:
524         *
525         * <ol>
526         * <li>the active global database — an in-script {@code USE database}, or the
527         * per-group database set by {@code DataFlowAnalyzer};</li>
528         * <li>the per-analysis {@code Option.getDefaultDatabase()};</li>
529         * <li>the attached {@code TSQLEnv} default catalog;</li>
530         * <li>{@code TSQLEnv.DEFAULT_DB_NAME}.</li>
531         * </ol>
532         *
533         * Each lower tier is consulted only when the higher tier is null/empty. This
534         * lets a shared multi-catalog {@code TSQLEnv} be reused across analyses with
535         * different per-analysis default databases (tier 2) without mutating the env,
536         * while still honoring an in-script {@code USE database} (tier 1).
537         */
538        public static String getEffectiveDefaultDatabase() {
539                // tier 1: active global database (USE database / per-group database)
540                String database = getGlobalDatabase();
541                if (!SQLUtil.isEmpty(database)) {
542                        return database;
543                }
544                // tier 2: per-analysis Option default database
545                Option option = getGlobalOption();
546                if (option != null) {
547                        String optionDatabase = option.getDefaultDatabase();
548                        if (!SQLUtil.isEmpty(optionDatabase) && !TSQLEnv.DEFAULT_DB_NAME.equals(optionDatabase)) {
549                                return optionDatabase;
550                        }
551                }
552                // tier 3: attached TSQLEnv default catalog
553                TSQLEnv sqlenv = getGlobalSQLEnv();
554                if (sqlenv != null && !SQLUtil.isEmpty(sqlenv.getDefaultCatalogName())) {
555                        return sqlenv.getDefaultCatalogName();
556                }
557                // tier 4: fallback
558                return TSQLEnv.DEFAULT_DB_NAME;
559        }
560
561        /**
562         * The schema an unqualified name defaults to right now.
563         *
564         * <p>Six model classes used to carry an identical copy of this fallback; it
565         * lives here so the {@link #isGlobalSchemaUnset() unset} rule cannot be
566         * applied to some of them and forgotten on the others. Returns
567         * {@link TSQLEnv#DEFAULT_SCHEMA_NAME} when there is no schema to default to.
568         *
569         * @see #getEffectiveDefaultDatabase()
570         */
571        public static String getEffectiveDefaultSchema() {
572                // A Snowflake USE <database> unset the current schema for this statement.
573                // The attached env's default schema is batch-global -- TUseSchema writes
574                // it while PARSING, so it holds the LAST USE SCHEMA of the whole script --
575                // and falling back to it here would re-attach the previous database's
576                // schema to names that follow the switch. See GitHub #715.
577                if (isGlobalSchemaUnset()) {
578                        // Unset, unless the attached catalog PROVES what the switch landed
579                        // in. Snowflake makes PUBLIC current when the new database has one;
580                        // when the catalog shows it does, that is a fact rather than an
581                        // assumption, and discarding it would lose an exact resolution.
582                        String afterSwitch = null;
583                        if (getGlobalSQLEnv() != null) {
584                                afterSwitch = getGlobalSQLEnv().findSchemaAfterDatabaseSwitch(getEffectiveDefaultDatabase());
585                        }
586                        return SQLUtil.isEmpty(afterSwitch) ? TSQLEnv.DEFAULT_SCHEMA_NAME : afterSwitch;
587                }
588                String defaultSchema = null;
589                if (getGlobalSQLEnv() != null) {
590                        defaultSchema = getGlobalSQLEnv().getDefaultSchemaName();
591                }
592                if (!SQLUtil.isEmpty(defaultSchema)) {
593                        return defaultSchema;
594                }
595                return TSQLEnv.DEFAULT_SCHEMA_NAME;
596        }
597
598        public static void setGlobalStmtStack(Stack<TCustomSqlStatement> stack) {
599                if (stack != null) {
600                        globalStmtStack.set(stack);
601                }
602        }
603
604        public static void removeGlobalStmtStack() {
605                globalStmtStack.remove();
606        }
607        
608        public static void setGlobalSqlInfo(IndexedLinkedHashMap<String, List<SqlInfo>> sqlInfoMap) {
609                if (sqlInfoMap != null) {
610                        globalSqlInfo.set(sqlInfoMap);
611                }
612        }
613
614        public static void removeGlobalSqlInfo() {
615                globalSqlInfo.remove();
616        }
617
618        public static IndexedLinkedHashMap<String, List<SqlInfo>> getGlobalSqlInfo() {
619                return (IndexedLinkedHashMap<String, List<SqlInfo>>) globalSqlInfo.get();
620        }
621
622        public static void setGlobalOption(Option option) {
623                if (option != null) {
624                        globalOption.set(option);
625                }
626        }
627        
628        public static Option getGlobalOption() {
629                return (Option) globalOption.get();
630        }
631
632        public static void removeGlobalOption() {
633                globalOption.remove();
634        }
635
636        public static Stack<TCustomSqlStatement> getGlobalStmtStack() {
637                return (Stack<TCustomSqlStatement>) globalStmtStack.get();
638        }
639
640        public static void remove() {
641                localManager.remove();
642                globalDatabase.remove();
643                globalSchema.remove();
644                globalDatabaseForm.remove();
645                globalSchemaForm.remove();
646                schemaUnsetStatements.remove();
647                globalVendor.remove();
648                globalSQLEnv.remove();
649                globalHash.remove();
650                globalStmtStack.remove();
651                globalOption.remove();
652                globalSqlInfo.remove();
653                globalIdentifierService.remove();
654        }
655
656    private IdentityHashMap<Object,Object> commentOccurrenceBindings;
657
658    /** Records the model actually selected by the factory, without changing its lookup maps. */
659    public <T> T observeCommentOccurrence(Object ast, T model) {
660        Option opt = getGlobalOption();
661        if (opt != null && opt.isCollectCommentBindings() && ast != null && model != null) {
662            if (commentOccurrenceBindings == null) commentOccurrenceBindings = new IdentityHashMap<Object,Object>();
663            commentOccurrenceBindings.put(ast, model);
664        }
665        return model;
666    }
667
668    /** Read existing AST bindings without name fallback, allocation, or ID changes. */
669    public void visitExactBindings(java.util.function.BiConsumer<Object, Object> visitor) {
670        if (commentOccurrenceBindings != null) {
671            try { commentOccurrenceBindings.forEach(visitor); }
672            finally { commentOccurrenceBindings = null; }
673        }
674        for (Map map : new Map[] { modelBindingMap, processModelBindingMap, viewModelBindingMap,
675                insertModelBindingMap, createModelBindingMap, mergeModelBindingMap, updateModelBindingMap }) {
676            for (Object item : map.entrySet()) {
677                Map.Entry entry = (Map.Entry) item;
678                visitor.accept(entry.getKey(), entry.getValue());
679            }
680        }
681    }
682
683        public void bindModel(Object gspModel, Object relationModel) {
684                if(gspModel == null) {
685                        return;
686                }
687                modelBindingMap.put(gspModel, relationModel);
688                if (gspModel instanceof TCustomSqlStatement && relationModel instanceof Procedure) {
689                        routineModels.put((TCustomSqlStatement) gspModel, (Procedure) relationModel);
690                }
691
692                TTable table = null;
693                if (gspModel instanceof TTable && !(gspModel instanceof TCTE)) {
694                        table = ((TTable) gspModel);
695                } else if (gspModel instanceof Table) {
696                        table = ((Table) gspModel).getTableObject();
697                } else if (gspModel instanceof QueryTable) {
698                        table = ((QueryTable) gspModel).getTableObject();
699                } else if (gspModel instanceof TCTE) {
700                        TTableList tables = ((TCTE) gspModel).getPreparableStmt().tables;
701                        for (int j = 0; j < tables.size(); j++) {
702                                TTable item = tables.getTable(j);
703                                if (item != null && !SQLUtil.isEmpty(item.getAliasName())) {
704                                        TCustomSqlStatement stmt = ((TCTE) gspModel).getPreparableStmt();
705                                        if (tableHashMap.containsKey(stmt)) {
706                                                tableAliasMap.put(aliasKey(tableHashMap.get(stmt), item.getAliasName()), item);
707                                        } else {
708                                                String sql = stmt.toString();
709                                                if(sql == null){
710                                                        System.err.println("Current statement toString() returns null.");
711                                                        continue;
712                                                }
713                                                String hash = SHA256.getMd5(sql);
714                                                tableHashMap.put(stmt, hash);
715                                                tableAliasMap.put(aliasKey(hash, item.getAliasName()), item);
716                                        }
717                                }
718
719                                if (item != null) {
720                                        tableSet.add(item);
721                                }
722                        }
723                } else if (gspModel instanceof Pair && ((Pair) gspModel).first instanceof Table) {
724                        table = ((Table) ((Pair) gspModel).first).getTableObject();
725                }
726
727                if (table == null && relationModel instanceof QueryTable) {
728                        table = ((QueryTable) relationModel).getTableObject();
729                }
730
731                updateTableAliasMap(table);
732
733                if (table != null) {
734                        tableSet.add(table);
735                }
736                
737                if(relationModel instanceof ResultSet) {
738                        resultSetSet.add(relationModel);
739                }
740        }
741
742        /** The alias map's key: the statement hash, and the alias (or the column's qualifier) as written. */
743        private static ScopedNameKey aliasKey(String statementHash, String aliasAsWritten) {
744                return new ScopedNameKey(statementHash, DlineageUtil.tableNameKey(aliasAsWritten));
745        }
746
747        protected void updateTableAliasMap(TTable table) {
748                if (table != null && !SQLUtil.isEmpty(table.getAliasName())) {
749                        TCustomSqlStatement stmt = ModelBindingManager.getGlobalStmtStack().peek();
750                        if (tableHashMap.containsKey(stmt)) {
751                                tableAliasMap.put(aliasKey(tableHashMap.get(stmt), table.getAliasName()), table);
752                        } else {
753                                String sql = stmt.toString();
754                                if(sql == null){
755                                        System.err.println("Current statement toString() returns null.");
756                                }
757                                else {
758                                        String hash = SHA256.getMd5(sql);
759                                        tableHashMap.put(stmt, hash);
760                                        tableAliasMap.put(aliasKey(hash, table.getAliasName()), table);
761                                }
762                        }
763                }
764        }
765
766        /**
767         * The model bound to {@code gspModel} itself, never one found by its name: a relation such as a LATERAL VIEW
768         * must not take the model of a table that has its name ({@link #getModel} falls back to the name of a TTable).
769         */
770        public Object getBoundModel(Object gspModel) {
771                return gspModel == null ? null : modelBindingMap.get(gspModel);
772        }
773
774        public Object getModel(Object gspModel) {
775                if (gspModel == null) {
776                        return null;
777                }
778
779                if (gspModel instanceof TTable && !(gspModel instanceof TCTE)) {
780                        TTable table = (TTable) gspModel;
781                        if (table.getCTE() != null) {
782                                Object result =  modelBindingMap.get(table.getCTE());
783                                if (result != null) {
784                                        return result;
785                                }
786                        }
787                        if (table.getAliasClause() != null && table.getAliasClause().getColumns() != null) {
788                                Object result = modelBindingMap.get(table.getAliasClause().getColumns());
789                                if (result != null) {
790                                        return result;
791                                }
792                        }
793                        if (table.getSubquery() != null && !table.getSubquery().isCombinedQuery()) {
794                                Object result =  modelBindingMap.get(table.getSubquery().getResultColumnList());
795                                if (result != null) {
796                                        return result;
797                                }
798                        }
799                }
800                if (gspModel instanceof TSelectSqlStatement) {
801                        TSelectSqlStatement select = (TSelectSqlStatement) gspModel;
802                        if (!select.isCombinedQuery()) {
803                                if (select.getResultColumnList() != null) {
804                                        Object result = modelBindingMap.get(select.getResultColumnList());
805                                        if (result != null) {
806                                                return result;
807                                        }
808                                }
809                                else if (select.getTransformClause() != null) {
810                                        Object result = modelBindingMap.get(select.getTransformClause());
811                                        if (result != null) {
812                                                return result;
813                                        }
814                                }
815                        }
816                }
817                Object result = modelBindingMap.get(gspModel);
818                if (result == null) {
819                        result = createModelBindingMap.get(gspModel);
820                }
821                if (result == null) {
822                        result = insertModelBindingMap.get(gspModel);
823                }
824                if (result == null) {
825                        result = updateModelBindingMap.get(gspModel);
826                }
827                if (result == null) {
828                        result = mergeModelBindingMap.get(gspModel);
829                }
830                if (result == null) {
831                        result = viewModelBindingMap.get(gspModel);
832                }
833                if (result == null) {
834                        result = cursorModelBindingMap.get(gspModel);
835                }
836
837                if (result == null && gspModel instanceof TTable) {
838                        result = getCreateTable((TTable) gspModel);
839                }
840
841                if (result == null && gspModel instanceof TTable) {
842                        result = (Table) getCreateModel((TTable) gspModel);
843                }
844
845                if (result == null && gspModel instanceof TTable) {
846                        TTable table = (TTable) gspModel;
847                        if (table.getLinkTable() != null) {
848                                result = (Table) getTableByName(
849                                                DlineageUtil.getTableIdentityKey(table.getLinkTable().getTableName().toString()));
850                                if (result == null) {
851                                        result = (Table) getTableByName(
852                                                        DlineageUtil.getTableIdentityKeyWithDefaultSchema(table.getLinkTable().getTableName().toString()));
853                                }
854                        } else {
855                                result = (Table) getTableByName(
856                                                DlineageUtil.getTableIdentityKey(((TTable) gspModel).getTableName().toString()));
857                                if (result == null) {
858                                        result = (Table) getTableByName(
859                                                        DlineageUtil.getTableIdentityKeyWithDefaultSchema(((TTable) gspModel).getTableName().toString()));
860                                }
861                        }
862                        TObjectName written = table.getLinkTable() != null ? table.getLinkTable().getTableName() : table.getTableName();
863                        // A table no FROM item or DML target declared (the qualifier of rec.field) names a local first; a FROM or
864                        // DML relation (FROM @t) is the table path's, else a local relation.
865                        boolean implicit = table.getEffectType() == null
866                                        || table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable;
867                        result = resolveMixedRelation(written == null ? null : written.toString(),
868                                        result instanceof Table ? (Table) result : null, implicit);
869                }
870                
871                if (result == null && gspModel instanceof TTable) {
872                        TTable table = (TTable) gspModel;
873                        if(table.getFuncCall()!=null) {
874                                result = getModel(table.getFuncCall());
875                        }
876                }
877                
878                if (result == null && gspModel instanceof TTable) {
879                        TTable table = (TTable) gspModel;
880                        Table tableModel = new Table(table);
881                        result = (Table) getTableByName(tableModel.getBindingKey());
882                }
883
884                if (gspModel instanceof TTable && result instanceof Table) {
885                        return visibleTemporaryModel((TTable) gspModel, (Table) result);
886                }
887                return result;
888        }
889
890        /** The named routine whose body holds a routine statement; an anonymous block is part of the one around it. */
891        private static TStoredProcedureSqlStatement namedRoutine(TStoredProcedureSqlStatement routine) {
892                for (TCustomSqlStatement current = routine; current != null; current = current.getParentStmt()) {
893                        if (current instanceof TStoredProcedureSqlStatement
894                                        && ((TStoredProcedureSqlStatement) current).getStoredProcedureName() != null) {
895                                return (TStoredProcedureSqlStatement) current;
896                        }
897                }
898                return routine;
899        }
900
901        /** Name-based caches must not override the metadata captured at a temporary reference. */
902        Table visibleTemporaryModel(TTable reference, Table candidate) {
903                if (candidate == null || !reference.isResolved()
904                                || !(reference.getSqlEnv() instanceof ProcedureEnvironment)) return candidate;
905                TTable origin = candidate.getTableObject();
906                if (origin == reference) return candidate;
907                if (origin != null && reference.getResolvedTable() == null && origin.getResolvedTable() != null
908                                && getGlobalProcedure() != null
909                                && namedRoutine(temporaryMetadataOwners.get(origin.getResolvedTable())) == namedRoutine(getGlobalProcedure())
910                                && gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics.ofName(reference.dbvendor, reference.getTableName())
911                                                == gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics.Scope.SESSION) {
912                        // This routine created the session table in SQL resolver2 never saw (EXECUTE of a
913                        // string). It outlives that statement, so the routine's later reference is to it.
914                        return candidate;
915                }
916                if (origin == null || origin.getResolvedTable() != reference.getResolvedTable()) return null;
917                if (reference.getResolvedTable() == null && origin.getSqlEnv() != reference.getSqlEnv()) return null;
918                return candidate;
919        }
920
921        public List<Procedure> getProcedureModels(){
922                List<Procedure> procedures = new ArrayList<Procedure>();
923                for (Object obj : modelBindingMap.values()) {
924                        if(obj instanceof Procedure && !procedures.contains(obj)){
925                                procedures.add((Procedure)obj);
926                        }
927                }
928                return procedures;
929        }
930        
931        public List<OraclePackage> getOraclePackageModels(){
932                List<OraclePackage> oraclePackages = new ArrayList<OraclePackage>();
933                for (Object obj : modelBindingMap.values()) {
934                        if(obj instanceof OraclePackage && !oraclePackages.contains(obj)){
935                                oraclePackages.add((OraclePackage)obj);
936                        }
937                }
938                return oraclePackages;
939        }
940        
941        public List<Process> getProcessModels(){
942                List<Process> processes = new ArrayList<Process>();
943                for (Object obj : processModelBindingMap.values()) {
944                        if(obj instanceof Process && !processes.contains(obj)){
945                                processes.add((Process)obj);
946                        }
947                }
948                return processes;
949        }
950        
951        public void bindProcessModel(TParseTreeNode gspModel, Object relationModel) {
952                processModelBindingMap.put(gspModel, relationModel);
953        }
954
955        public void unbindProcessModel(TParseTreeNode gspModel) {
956                processModelBindingMap.remove(gspModel);
957        }
958
959        public Process getProcessModel(TParseTreeNode gspModel) {
960                return (Process)processModelBindingMap.get(gspModel);
961        }
962        
963        public void bindViewModel(Object gspModel, Object relationModel) {
964                viewModelBindingMap.put(gspModel, relationModel);
965        }
966        
967        public Object getViewModel(Object gspModel) {
968                return viewModelBindingMap.get(gspModel);
969        }
970
971        public void bindUpdateModel(Object gspModel, Object relationModel) {
972                updateModelBindingMap.put(gspModel, relationModel);
973        }
974
975        public Object getUpdateModel(Object gspModel) {
976                return updateModelBindingMap.get(gspModel);
977        }
978
979        public void bindMergeModel(Object gspModel, Object relationModel) {
980                mergeModelBindingMap.put(gspModel, relationModel);
981        }
982
983        public Object getMergeModel(Object gspModel) {
984                return mergeModelBindingMap.get(gspModel);
985        }
986
987        public void bindInsertModel(Object gspModel, Object relationModel) {
988                insertModelBindingMap.put(gspModel, relationModel);
989        }
990
991        public Object getInsertModel(Object gspModel) {
992                return insertModelBindingMap.get(gspModel);
993        }
994
995        /** Adds one of a routine's function results under the routine's name as written. */
996        public void bindFunctionResult(RoutineAliasKey key, Object functionTable) {
997                if (key == null) return;
998                noteRoutineService(key);
999                Set<Object> tableSet = functionTableAliases.get(key);
1000                if (tableSet == null) {
1001                        tableSet = new LinkedHashSet<Object>();
1002                        functionTableAliases.put(key, tableSet);
1003                }
1004                tableSet.add(functionTable);
1005        }
1006
1007        /** A routine's function results, by its name as written (the exact name, else the other shape's routine level). */
1008        public Set<Object> getFunctionResults(RoutineAliasKey key) {
1009                return functionTableAliases.lookupName(key);
1010        }
1011
1012        /**
1013         * Adds a function result under a key of the deprecated text-keyed API: the key as given (any object), and, for text,
1014         * also the routine name the text reads as, so the analysis's own lookups see it.
1015         *
1016         * @deprecated function results are keyed by the routine's name as written: use
1017         *             {@link #bindFunctionResult(RoutineAliasKey, Object)}
1018         */
1019        @Deprecated
1020        public void bindTableFunction(Object gspModel, Object functionTable) {
1021                Set<Object> tableSet = legacyFunctionTables.get(gspModel);
1022                if (tableSet == null) {
1023                        tableSet = new LinkedHashSet<Object>();
1024                        legacyFunctionTables.put(gspModel, tableSet);
1025                }
1026                tableSet.add(functionTable);
1027                if (gspModel instanceof String) {
1028                        bindFunctionResult(legacyTextKey((String) gspModel), functionTable);
1029                }
1030        }
1031
1032        /**
1033         * The function results under a key of the deprecated text-keyed API, looked up as the legacy map was (the key as
1034         * given; for text, a qualified key's last segment against an unqualified one, as stored names); then, for text,
1035         * under the routine name the text reads as.
1036         *
1037         * @deprecated use {@link #getFunctionResults(RoutineAliasKey)}
1038         */
1039        @Deprecated
1040        public Set<Object> getFunctionTable(Object gspModel) {
1041                Set<Object> result = legacyFunctionTables.get(gspModel);
1042                if (result == null && gspModel instanceof String) {
1043                        String modelString = (String) gspModel;
1044                        for (Map.Entry<Object, Set<Object>> entry : legacyFunctionTables.entrySet()) {
1045                                if (!(entry.getKey() instanceof String)) {
1046                                        continue;
1047                                }
1048                                String keyString = (String) entry.getKey();
1049                                if (keyString.indexOf(".") != -1 && modelString.indexOf(".") == -1
1050                                                && sameStoredBindingName(ESQLDataObjectType.dotTable,
1051                                                                keyString.substring(keyString.lastIndexOf(".") + 1), modelString)) {
1052                                        return entry.getValue();
1053                                }
1054                                if (keyString.indexOf(".") == -1 && modelString.indexOf(".") != -1
1055                                                && sameStoredBindingName(ESQLDataObjectType.dotTable, keyString,
1056                                                                modelString.substring(modelString.lastIndexOf(".") + 1))) {
1057                                        return entry.getValue();
1058                                }
1059                        }
1060                        result = getFunctionResults(legacyTextKey(modelString));
1061                }
1062                return result;
1063        }
1064
1065        /** Stored-name equality under the analysis's rules, for the deprecated text-keyed API's legacy lookups. */
1066        private boolean sameStoredBindingName(ESQLDataObjectType type, String a, String b) {
1067                gudusoft.gsqlparser.sqlenv.IdentifierService service = getGlobalIdentifierService();
1068                if (service == null) return a.equalsIgnoreCase(b); // non-identifier-compare: no vendor or configuration is known on this thread, so no identifier rules apply; the legacy maps' last-resort fallback
1069                return service.canonKeyOf(a, type, gudusoft.gsqlparser.sqlenv.IdentifierInputForm.CATALOG_STORED)
1070                        .equals(service.canonKeyOf(b, type, gudusoft.gsqlparser.sqlenv.IdentifierInputForm.CATALOG_STORED));
1071        }
1072
1073        public Table getCreateTable(TTable table) {
1074                Table scoped = getTemporaryMetadataModel(table);
1075                if (scoped != null && temporaryMetadataOwners.get(table.getResolvedTable()) != getGlobalProcedure()) return scoped;
1076                if (table != null && table.getFullName() != null) {
1077                        return visibleTemporaryModel(table, (Table) createModelQuickBindingMap.get(
1078                                        DlineageUtil.getTableIdentityKey(table.getTableName().toString())));
1079                }
1080                if(modelBindingMap.get(table) instanceof Table) {
1081                        return (Table)modelBindingMap.get(table);
1082                }
1083                return null;
1084        }
1085
1086        public Table getCreateModel(TTable table) {
1087                return (Table) createModelBindingMap.get(table);
1088        }
1089
1090        Table getTemporaryMetadataModel(TTable table) {
1091                return isScopedTemporary(table) ? temporaryMetadataModels.get(table.getResolvedTable()) : null;
1092        }
1093
1094        /**
1095         * Whether a table reference names one routine body's temporary table rather than a shared
1096         * one: its name says so ({@code #t}), or resolver2 bound it to a table that a routine
1097         * declared temporary ({@code CREATE TEMP TABLE t}). See
1098         * {@link gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics}.
1099         */
1100        public static boolean isScopedTemporary(TTable table) {
1101                return table != null && table.getTableName() != null
1102                                && (table.getSqlEnv() instanceof ProcedureEnvironment
1103                                || SQLUtil.isLocalTemporaryTable(table.dbvendor, table.getTableName()));
1104        }
1105
1106        public void bindCreateModel(TTable table, Table tableModel) {
1107                createModelBindingMap.put(table, tableModel);
1108                Object key = DlineageUtil.getTableIdentityKey(table.getTableName().toString());
1109                if ((getGlobalProcedure() != null && isScopedTemporary(table)) || !createModelQuickBindingMap.containsKey(key)) {
1110                        displaceBinding(createModelQuickBindingMap, 1, key, tableModel);
1111                        createModelQuickBindingMap.put(key, tableModel);
1112                }
1113        }
1114
1115        public void bindCreateModel(TObjectName tableName, Table tableModel) {
1116                if (!createModelQuickBindingMap.containsKey(DlineageUtil.getTableIdentityKey(tableName.toString()))) {
1117                        createModelQuickBindingMap.put(DlineageUtil.getTableIdentityKey(tableName.toString()), tableModel);
1118                }
1119        }
1120        
1121        public void bindCreateModel(String tableName, Table tableModel) {
1122                if (!createModelQuickBindingMap.containsKey(DlineageUtil.getTableIdentityKey(tableName))) {
1123                        createModelQuickBindingMap.put(DlineageUtil.getTableIdentityKey(tableName), tableModel);
1124                }
1125        }
1126
1127        public TObjectName[] getTableColumns(TTable table) {
1128                Table createTable = getCreateTable(table);
1129                if (createTable != null) {
1130                        List<TableColumn> columnList = createTable.getColumns();
1131                        TObjectName[] columns = new TObjectName[columnList.size()];
1132                        for (int i = 0; i < columns.length; i++) {
1133                                columns[i] = columnList.get(i).getColumnObject();
1134                        }
1135                        Arrays.sort(columns, new Comparator<TObjectName>() {
1136
1137                                @Override
1138                                public int compare(TObjectName o1, TObjectName o2) {
1139                                        if (o1 == null) {
1140                                                return -1;
1141                                        }
1142                                        if (o2 == null) {
1143                                                return 1;
1144                                        }
1145                                        return o1.getStartToken().posinlist - o2.getStartToken().posinlist;
1146                                }
1147                        });
1148                        return columns;
1149                }
1150                else if(modelBindingMap.get(table) instanceof QueryTable) {
1151                        QueryTable queryTable = (QueryTable)modelBindingMap.get(table);
1152                        List<TObjectName> columns = new ArrayList<TObjectName>();
1153                        for (ResultColumn resultColumn : queryTable.getColumns()) {
1154                                if(resultColumn.hasStarLinkColumn()) {
1155                                        for(TObjectName column: resultColumn.getStarLinkColumnList()) {
1156                                                columns.add(column);
1157                                        }
1158                                }
1159                                else if (resultColumn.getColumnObject() instanceof TResultColumn) {
1160                                        TResultColumn columnObject = ((TResultColumn) resultColumn.getColumnObject());
1161                                        TAliasClause alias = columnObject.getAliasClause();
1162                                        if (alias != null && alias.getAliasName() != null) {
1163                                                if(resultColumn.isStruct() && resultColumn.isStarColumn()) {
1164                                                        TObjectName structColumn = DlineageUtil.syntheticStarName();
1165                                                        columns.add(structColumn);
1166                                                }
1167                                                else {
1168                                                        columns.add(alias.getAliasName());
1169                                                }
1170                                        } else {
1171                                                if (columnObject.getFieldAttr() != null) {
1172                                                        columns.add(columnObject.getFieldAttr());
1173                                                } else {
1174                                                        TObjectName column = new TObjectName();
1175                                                        if (columnObject.getExpr().getExpressionType() == EExpressionType.typecast_t) {
1176                                                                column.setString(columnObject.getExpr().getLeftOperand().toString());
1177                                                        } else {
1178                                                                column.setString(columnObject.toString());
1179                                                        }
1180                                                        columns.add(column);
1181                                                }
1182                                        }
1183                                } else if (resultColumn.getColumnObject() instanceof TObjectName) {
1184                                        columns.add((TObjectName) resultColumn.getColumnObject());
1185                                }
1186                        }
1187                        return columns.toArray(new TObjectName[0]);
1188                }
1189
1190                TObjectNameList list = table.getLinkedColumns();
1191                List<TObjectName> columns = new ArrayList<TObjectName>();
1192
1193                if (table.getCTE() != null) {
1194                        ResultSet resultSet = (ResultSet) getModel(table.getCTE());
1195                        if (resultSet != null) {
1196                                List<ResultColumn> columnList = resultSet.getColumns();
1197                                for (int i = 0; i < columnList.size(); i++) {
1198                                        ResultColumn resultColumn = columnList.get(i);
1199                                        if (resultColumn.getColumnObject() instanceof TResultColumn) {
1200                                                TResultColumn columnObject = ((TResultColumn) resultColumn.getColumnObject());
1201                                                TAliasClause alias = columnObject.getAliasClause();
1202                                                if (alias != null && alias.getAliasName() != null) {
1203                                                        columns.add(alias.getAliasName());
1204                                                } else {
1205                                                        if (columnObject.getFieldAttr() != null) {
1206                                                                columns.add(columnObject.getFieldAttr());
1207                                                        } else {
1208                                                                continue;
1209                                                        }
1210                                                }
1211                                        } else if (resultColumn.getColumnObject() instanceof TObjectName) {
1212                                                columns.add((TObjectName) resultColumn.getColumnObject());
1213                                        }
1214                                }
1215                        }
1216                } else if (list.size() == 1 && DlineageUtil.isStarName(list.getObjectName(0)) && table.getSubquery() != null) {
1217                        addSubqueryColumns(table, columns);
1218                } else {
1219                        for (int i = 0; i < list.size(); i++) {
1220                                TObjectName object = list.getObjectName(i);
1221                                if (DlineageUtil.isStarName(object) && table.getSubquery() != null) {
1222                                        addSubqueryColumns(table, columns);
1223                                }
1224                                else 
1225                                        columns.add(object);
1226                        }
1227                }
1228                Collections.sort(columns, new Comparator<TObjectName>() {
1229
1230                        @Override
1231                        public int compare(TObjectName o1, TObjectName o2) {
1232                                return o1.getStartToken().posinlist - o2.getStartToken().posinlist;
1233                        }
1234                });
1235                return columns.toArray(new TObjectName[0]);
1236        }
1237
1238        private void addSubqueryColumns(TTable table, List<TObjectName> columns) {
1239                ResultSet resultSet = (ResultSet) getModel(table.getSubquery());
1240                if (resultSet != null) {
1241                        List<ResultColumn> columnList = resultSet.getColumns();
1242                        for (int i = 0; i < columnList.size(); i++) {
1243                                ResultColumn resultColumn = columnList.get(i);
1244                                if (resultColumn.getColumnObject() instanceof TResultColumn) {
1245                                        TResultColumn columnObject = ((TResultColumn) resultColumn.getColumnObject());
1246                                        TAliasClause alias = columnObject.getAliasClause();
1247                                        if (alias != null && alias.getAliasName() != null) {
1248                                                columns.add(alias.getAliasName());
1249                                        } else {
1250                                                if (columnObject.getFieldAttr() != null) {
1251                                                        columns.add(columnObject.getFieldAttr());
1252                                                } else {
1253                                                        TObjectName column = new TObjectName();
1254                                                        if (columnObject.getExpr().getExpressionType() == EExpressionType.typecast_t) {
1255                                                                column.setString(columnObject.getExpr().getLeftOperand().toString());
1256                                                        } else {
1257                                                                column.setString(columnObject.toString());
1258                                                        }
1259                                                        columns.add(column);
1260                                                }
1261                                        }
1262                                } else if (resultColumn.getColumnObject() instanceof TObjectName) {
1263                                        columns.add((TObjectName) resultColumn.getColumnObject());
1264                                }
1265                        }
1266                }
1267        }
1268
1269        public TTable getTableFromColumn(TObjectName column) {
1270                if (column.getSourceTable() != null) {
1271                        return column.getSourceTable();
1272                }
1273
1274                if (column.getTableString() != null && column.getTableString().trim().length() > 0) {
1275                        TCustomSqlStatement peekStmt = ModelBindingManager.getGlobalStmtStack().peek();
1276                        while (peekStmt != null) {
1277                                TTable table = null;
1278                                if (tableHashMap.containsKey(peekStmt)) {
1279                                        table = tableAliasMap.get(aliasKey(tableHashMap.get(peekStmt), column.getTableString()));
1280                                }
1281                                if (table != null) {
1282                                        return table;
1283                                } else {
1284                                        peekStmt = peekStmt.getParentStmt();
1285                                }
1286                        }
1287                }
1288                return null;
1289        }
1290        
1291        public TTable getTable(TCustomSqlStatement stmt, TObjectName column) {
1292                if (column.getSourceTable() != null) {
1293                        return column.getSourceTable();
1294                }
1295
1296                if (column.getTableString() != null && column.getTableString().trim().length() > 0) {
1297                        TCustomSqlStatement peekStmt = ModelBindingManager.getGlobalStmtStack().peek();
1298                        while (peekStmt != null) {
1299                                TTable table = null;
1300                                if (tableHashMap.containsKey(peekStmt)) {
1301                                        table = tableAliasMap.get(aliasKey(tableHashMap.get(peekStmt), column.getTableString()));
1302                                }
1303                                if (table != null && table.getSubquery() != stmt)
1304                                        return table;
1305                                
1306                                if (peekStmt.getTables() != null) {
1307                                        for (TTable tableItem : peekStmt.getTables()) {
1308                                                if (DlineageUtil.sameQualifiedName(ESQLDataObjectType.dotTable, tableItem.getTableName().toString(),
1309                                                                column.getTableString())) {
1310                                                        return tableItem;
1311                                                }
1312                                        }
1313                                }
1314                                
1315                                peekStmt = peekStmt.getParentStmt();
1316                        }
1317                }
1318
1319                if (stmt.getTables() != null) {
1320                        for (int j = 0; j < stmt.getTables().size(); j++) {
1321                                TTable table = (TTable) stmt.getTables().getTable(j);
1322                                if (table.getSubquery() == stmt)
1323                                        continue;
1324
1325                                TObjectName[] columns = getTableColumns(table);
1326                                for (int i = 0; i < columns.length; i++) {
1327                                        TObjectName columnName = columns[i];
1328                                        if (columnName == null || DlineageUtil.isWildcard(columnName))
1329                                                continue;
1330                                        if (DlineageUtil.sameQualifiedName(ESQLDataObjectType.dotColumn, columnName.toString(), column.toString())) {
1331                                                if (columnName.getSourceTable() == null || columnName.getSourceTable() == table) {
1332                                                        return decided(SqllrObservations.DECISION_FIRST_TABLE_WITH_COLUMN, column, table);
1333                                                }
1334                                        }
1335                                        if (!SQLUtil.isEmpty(columnName.getColumnNameOnly())
1336                                                        && DlineageUtil.sameName(ESQLDataObjectType.dotColumn, columnName.getColumnNameOnly(),
1337                                                                        column.getColumnNameOnly())) {
1338                                                if (columnName.getSourceTable() == null || columnName.getSourceTable() == table) {
1339                                                        return decided(SqllrObservations.DECISION_FIRST_TABLE_WITH_COLUMN, column, table);
1340                                                }
1341                                        }
1342                                }
1343                        }
1344                }
1345
1346                return null;
1347        }
1348
1349        public TTable guessTable(TCustomSqlStatement stmt, TObjectName column) {
1350                if (column.getTableString() != null && column.getTableString().trim().length() > 0) {
1351                        TCustomSqlStatement peekStmt = ModelBindingManager.getGlobalStmtStack().peek();
1352                        TTable table = null;
1353                        if (tableHashMap.containsKey(peekStmt)) {
1354                                table = tableAliasMap.get(aliasKey(tableHashMap.get(peekStmt), column.getTableString()));
1355                        }
1356                        if (table != null && table.getSubquery() != stmt)
1357                                return table;
1358                }
1359
1360                Iterator iter = tableSet.iterator();
1361                while (iter.hasNext()) {
1362                        TTable table = (TTable) iter.next();
1363
1364                        if (table.getSubquery() == stmt)
1365                                continue;
1366
1367                        TObjectName[] columns = getTableColumns(table);
1368                        for (int i = 0; i < columns.length; i++) {
1369                                TObjectName columnName = columns[i];
1370                                if (DlineageUtil.isWildcard(columnName))
1371                                        continue;
1372                                if (DlineageUtil.sameQualifiedName(ESQLDataObjectType.dotColumn, columnName.toString(), column.toString())) {
1373                                        if (columnName.getSourceTable() == null || columnName.getSourceTable() == table) {
1374                                                return decided(SqllrObservations.DECISION_GUESS_TABLE, column, table);
1375                                        }
1376                                }
1377                        }
1378                }
1379                return null;
1380        }
1381
1382        /** Records a fallback binding for the SQL-LR producer's snapshot (observe-only) and returns the table. */
1383        private static TTable decided(String id, TObjectName column, TTable table) {
1384                SqllrObservations observations = SqllrObservations.current();
1385                if (observations != null) {
1386                        observations.decision(id, column, table, "a column-name scan over the FROM items");
1387                }
1388                return table;
1389        }
1390
1391        public List<Table> getTablesByName() {
1392                // A table bound by identity is also bound under its legacy name; list it once, at that
1393                // name, as before. Only a table whose legacy name now belongs to another table (two
1394                // tables the analysis keeps apart but the vendor-default fold merges) is listed by its
1395                // canonical key.
1396                Set<Object> alsoBoundByName = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
1397                for (Object entry : tableNamesMap.entrySet()) {
1398                        Map.Entry binding = (Map.Entry) entry;
1399                        if (!(binding.getKey() instanceof QualifiedCanonKey)) {
1400                                alsoBoundByName.add(binding.getValue());
1401                        }
1402                }
1403                List<Table> tables = new ArrayList<Table>();
1404                for (Object entry : tableNamesMap.entrySet()) {
1405                        Map.Entry binding = (Map.Entry) entry;
1406                        if (binding.getKey() instanceof QualifiedCanonKey && getGlobalVendor() != EDbVendor.dbvbigquery
1407                                        && alsoBoundByName.contains(binding.getValue())) {
1408                                continue;
1409                        }
1410                        tables.add((Table) binding.getValue());
1411                }
1412                return tables;
1413        }
1414
1415        public List<TTable> getBaseTables() {
1416                List<TTable> tables = new ArrayList<TTable>();
1417
1418                Iterator iter = modelBindingMap.keySet().iterator();
1419                while (iter.hasNext()) {
1420                        Object key = iter.next();
1421                        if (!(key instanceof TTable)) {
1422                                continue;
1423                        }
1424                        TTable table = (TTable) key;
1425                        if (table.getSubquery() == null) {
1426                                tables.add(table);
1427                        }
1428                }
1429
1430                iter = createModelBindingMap.keySet().iterator();
1431                while (iter.hasNext()) {
1432                        Object key = iter.next();
1433                        if (!(key instanceof TTable)) {
1434                                continue;
1435                        }
1436                        TTable table = (TTable) key;
1437                        tables.add(table);
1438                }
1439
1440                iter = insertModelBindingMap.keySet().iterator();
1441                while (iter.hasNext()) {
1442                        Object key = iter.next();
1443                        if (!(key instanceof TTable)) {
1444                                continue;
1445                        }
1446                        TTable table = (TTable) key;
1447                        tables.add(table);
1448                }
1449
1450                iter = mergeModelBindingMap.keySet().iterator();
1451                while (iter.hasNext()) {
1452                        Object key = iter.next();
1453                        if (!(key instanceof TTable)) {
1454                                continue;
1455                        }
1456                        TTable table = (TTable) key;
1457                        tables.add(table);
1458                }
1459
1460                iter = updateModelBindingMap.keySet().iterator();
1461                while (iter.hasNext()) {
1462                        Object key = iter.next();
1463                        if (!(key instanceof TTable)) {
1464                                continue;
1465                        }
1466                        TTable table = (TTable) key;
1467                        if (!tables.contains(table)) {
1468                                tables.add(table);
1469                        }
1470                }
1471
1472                return tables;
1473        }
1474
1475        public List<TCustomSqlStatement> getViews() {
1476                List<TCustomSqlStatement> views = new ArrayList<TCustomSqlStatement>();
1477
1478                Iterator iter = viewModelBindingMap.keySet().iterator();
1479                while (iter.hasNext()) {
1480                        Object key = iter.next();
1481                        if (!(key instanceof TCreateViewSqlStatement) && !(key instanceof TCreateMaterializedSqlStatement)) {
1482                                continue;
1483                        }
1484                        TCustomSqlStatement view = (TCustomSqlStatement) key;
1485                        if (!views.contains(view)) {
1486                                views.add(view);
1487                        }
1488                }
1489                return views;
1490        }
1491
1492        public List<TResultColumnList> getSelectResultSets() {
1493                List<TResultColumnList> resultSets = new ArrayList<TResultColumnList>();
1494
1495                Iterator iter = modelBindingMap.keySet().iterator();
1496                while (iter.hasNext()) {
1497                        Object key = iter.next();
1498                        if (!(key instanceof TResultColumnList)) {
1499                                continue;
1500                        }
1501                        TResultColumnList resultset = (TResultColumnList) key;
1502                        resultSets.add(resultset);
1503                }
1504                return resultSets;
1505        }
1506
1507        public List<TObjectNameList> getQueryAliasTables() {
1508                List<TObjectNameList> resultSets = new ArrayList<TObjectNameList>();
1509
1510                Iterator iter = modelBindingMap.keySet().iterator();
1511                while (iter.hasNext()) {
1512                        Object key = iter.next();
1513                        if (!(key instanceof TObjectNameList)) {
1514                                continue;
1515                        }
1516                        TObjectNameList resultset = (TObjectNameList) key;
1517                        resultSets.add(resultset);
1518                }
1519                return resultSets;
1520        }
1521
1522        public List<TSelectSqlStatement> getSelectSetResultSets() {
1523                List<TSelectSqlStatement> resultSets = new ArrayList<TSelectSqlStatement>();
1524
1525                Iterator iter = modelBindingMap.keySet().iterator();
1526                while (iter.hasNext()) {
1527                        Object key = iter.next();
1528                        if (!(key instanceof TSelectSqlStatement)) {
1529                                continue;
1530                        }
1531
1532                        TSelectSqlStatement stmt = (TSelectSqlStatement) key;
1533                        if (stmt.getSetOperatorType() == ESetOperatorType.none)
1534                                continue;
1535
1536                        resultSets.add(stmt);
1537                }
1538                return resultSets;
1539        }
1540
1541        public List<TCTE> getCTEs() {
1542                List<TCTE> resultSets = new ArrayList<TCTE>();
1543
1544                Iterator iter = modelBindingMap.keySet().iterator();
1545                while (iter.hasNext()) {
1546                        Object key = iter.next();
1547                        if (!(key instanceof TCTE)) {
1548                                continue;
1549                        }
1550
1551                        TCTE cte = (TCTE) key;
1552                        resultSets.add(cte);
1553                }
1554                return resultSets;
1555        }
1556
1557        public List<TTable> getTableWithSelectSetResultSets() {
1558                List<TTable> resultSets = new ArrayList<TTable>();
1559
1560                Iterator iter = modelBindingMap.keySet().iterator();
1561                while (iter.hasNext()) {
1562                        Object key = iter.next();
1563                        if (!(key instanceof TTable)) {
1564                                continue;
1565                        }
1566
1567                        if (((TTable) key).getSubquery() == null)
1568                                continue;
1569                        TSelectSqlStatement stmt = ((TTable) key).getSubquery();
1570                        if (stmt.getSetOperatorType() == ESetOperatorType.none)
1571                                continue;
1572
1573                        resultSets.add((TTable) key);
1574                }
1575                return resultSets;
1576        }
1577
1578        public List<TParseTreeNode> getMergeResultSets() {
1579                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1580
1581                Iterator iter = modelBindingMap.keySet().iterator();
1582                while (iter.hasNext()) {
1583                        Object key = iter.next();
1584                        if (!(key instanceof TMergeUpdateClause) && !(key instanceof TMergeInsertClause)) {
1585                                continue;
1586                        }
1587                        TParseTreeNode resultset = (TParseTreeNode) key;
1588                        resultSets.add(resultset);
1589                }
1590                return resultSets;
1591        }
1592
1593        public List<TParseTreeNode> getOutputResultSets() {
1594                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1595
1596                Iterator iter = modelBindingMap.keySet().iterator();
1597                while (iter.hasNext()) {
1598                        Object key = iter.next();
1599                        if (!(key instanceof TOutputClause)) {
1600                                continue;
1601                        }
1602                        TParseTreeNode resultset = (TParseTreeNode) key;
1603                        resultSets.add(resultset);
1604                }
1605                return resultSets;
1606        }
1607
1608        public List<TParseTreeNode> getUpdateResultSets() {
1609                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1610
1611                Iterator iter = modelBindingMap.keySet().iterator();
1612                while (iter.hasNext()) {
1613                        Object key = iter.next();
1614                        if (!(key instanceof TUpdateSqlStatement)) {
1615                                continue;
1616                        }
1617                        TParseTreeNode resultset = (TParseTreeNode) key;
1618                        resultSets.add(resultset);
1619                }
1620                return resultSets;
1621        }
1622
1623        public List<TParseTreeNode> getFunctoinCalls() {
1624                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1625
1626                Iterator iter = modelBindingMap.keySet().iterator();
1627                while (iter.hasNext()) {
1628                        Object key = iter.next();
1629                        if (!(key instanceof TFunctionCall) && !(key instanceof TCaseExpression)) {
1630                                continue;
1631                        }
1632                        TParseTreeNode resultset = (TParseTreeNode) key;
1633                        resultSets.add(resultset);
1634                }
1635                return resultSets;
1636        }
1637
1638        public List<TParseTreeNode> getAliases() {
1639                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1640
1641                Iterator iter = modelBindingMap.keySet().iterator();
1642                while (iter.hasNext()) {
1643                        Object key = iter.next();
1644                        if (!(key instanceof TAliasClause)) {
1645                                continue;
1646                        }
1647                        TParseTreeNode resultset = (TParseTreeNode) key;
1648                        resultSets.add(resultset);
1649                }
1650                return resultSets;
1651        }
1652        
1653        public List<TParseTreeNode> getCursors() {
1654                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1655
1656                Iterator iter = modelBindingMap.keySet().iterator();
1657                while (iter.hasNext()) {
1658                        Object key = iter.next();
1659                        if (!(key instanceof TCursorDeclStmt) && !(key instanceof TOpenforStmt)) {
1660                                continue;
1661                        }
1662                        TParseTreeNode resultset = (TParseTreeNode) key;
1663                        resultSets.add(resultset);
1664                }
1665                return resultSets;
1666        }
1667        
1668        public List<TParseTreeNode> getPivotdTables() {
1669                List<TParseTreeNode> resultSets = new ArrayList<TParseTreeNode>();
1670
1671                Iterator iter = modelBindingMap.keySet().iterator();
1672                while (iter.hasNext()) {
1673                        Object key = iter.next();
1674                        if (!(key instanceof TPivotClause)) {
1675                                continue;
1676                        }
1677                        TParseTreeNode resultset = (TParseTreeNode) key;
1678                        resultSets.add(resultset);
1679                }
1680                return resultSets;
1681        }
1682
1683        public void addRelation(Relationship relation) {
1684                if (relation != null && !relationHolder.contains(relation)) {
1685                        relationHolder.add(relation);
1686                }
1687        }
1688        
1689        public void removeRelation(ImpactRelationship relation) {
1690                if (relation != null) {
1691                        relationHolder.remove(relation);
1692                }
1693        }
1694
1695        public Relationship[] getRelations() {
1696                return relationHolder.toArray(new Relationship[0]);
1697        }
1698
1699        public void reset() {
1700        currentStatementOrdinal = -1;
1701        commentOccurrenceBindings = null;
1702                modelBindingMap.clear();
1703                processModelBindingMap.clear();
1704                viewModelBindingMap.clear();
1705                insertModelBindingMap.clear();
1706                createModelBindingMap.clear();
1707                createModelQuickBindingMap.clear();
1708                mergeModelBindingMap.clear();
1709                updateModelBindingMap.clear();
1710                cursorModelBindingMap.clear();
1711                tableNamesMap.clear();
1712                temporaryScopes.clear();
1713                identityBoundTables.clear();
1714                setContestedNormalNames(null, null);
1715                legacyNormalNames = false;
1716                dropTables.clear();
1717                routineModels.clear();
1718                routineShadowState = null;
1719                procedureAliases.clear();
1720                packageAliases.clear();
1721                functionTableAliases.clear();
1722                legacyProcedureNames.clear();
1723                legacyPackageNames.clear();
1724                legacyFunctionTables.clear();
1725                routineService = null;
1726                localSymbols.clear();
1727                localCursorModels.clear();
1728                poolStack.clear();
1729                relationHolder.clear();
1730                tableAliasMap.clear();
1731                tableHashMap.clear();
1732                topStmtHashMap.clear();
1733                hashSQLMap.clear();
1734                dynamicSQLMap.clear();
1735                tableSet.clear();
1736                resultSetSet.clear();
1737                dblinkTableSet.clear();
1738                objectTypeIndex.clear();
1739                collectionToElementTypeIndex.clear();
1740                pipelinedSignatures.clear();
1741                pendingPipelinedCallSites.clear();
1742                resolvingPipelinedFunctions.clear();
1743                DISPLAY_ID.clear();
1744                DISPLAY_NAME.clear();
1745                virtualTableNames.clear();
1746        }
1747
1748        public void bindCursorModel(TCursorDeclStmt stmt, Variable resultSet) {
1749                bindCursorModel(stmt, stmt.getCursorName(), resultSet);
1750        }
1751
1752        public void bindCursorModel(TMssqlDeclare stmt, Variable resultSet) {
1753                bindCursorModel(stmt, stmt.getCursorName(), resultSet);
1754        }
1755
1756        public void bindCursorModel(TOpenforStmt stmt, Variable resultSet) {
1757                bindCursorModel(stmt, stmt.getCursorVariableName(), resultSet);
1758        }
1759
1760        public void bindCursorModel(TForStmt stmt, Variable resultSet) {
1761                bindCursorModel(stmt, stmt.getCursorName(), resultSet);
1762        }
1763
1764        public void bindCursorModel(TLoopStmt stmt, Variable resultSet) {
1765                bindCursorModel(stmt, stmt.getRecordName(), resultSet);
1766        }
1767
1768        /** A cursor's model, by the cursor's name as written in the scope of the statement that declares or opens it. */
1769        private void bindCursorModel(TCustomSqlStatement stmt, TObjectName cursorName, Variable resultSet) {
1770                gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key = cursorName == null ? null
1771                                : localCursorKey(stmt, cursorName.toString());
1772                if (key != null) {
1773                        localCursorModels.put(key, resultSet);
1774                }
1775        }
1776
1777        /**
1778         * Binds a cursor FOR loop's index to the cursor the loop names: one declared in the loop's routine, an enclosing
1779         * routine or the package.
1780         */
1781        public void bindCursorIndex(TObjectName indexName, TObjectName cursorName) {
1782                TCustomSqlStatement stmt = getGlobalStmtStack().peek();
1783                gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey cursorKey = localCursorKey(stmt, cursorName.toString());
1784                Table cursor = cursorKey == null ? null : firstVisible(localCursorModels, cursorKey,
1785                                gudusoft.gsqlparser.dlineage.symbol.LocalScopes.chainOf(this, stmt), false);
1786                if (cursor instanceof Variable) {
1787                        publishLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.of(cursorKey.getScope(),
1788                                        stripColon(indexName.toString()), gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.VARIABLE),
1789                                        cursor);
1790                }
1791        }
1792
1793        /** A cursor's key: its name as written, in the scope of the statement that names it. */
1794        private gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey localCursorKey(TCustomSqlStatement stmt, String cursorName) {
1795                return gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.of(
1796                                gudusoft.gsqlparser.dlineage.symbol.LocalScopes.scopeOf(this, stmt), stripColon(cursorName),
1797                                gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.CURSOR);
1798        }
1799
1800        private static String stripColon(String name) {
1801                return name != null && name.startsWith(":") ? name.substring(1) : name;
1802        }
1803
1804        public List<TStoredProcedureSqlStatement> getProcedures() {
1805                List<TStoredProcedureSqlStatement> procedures = new ArrayList();
1806                Iterator iter = this.modelBindingMap.keySet().iterator();
1807
1808                while (iter.hasNext()) {
1809                        Object key = iter.next();
1810                        if (key instanceof TStoredProcedureSqlStatement) {
1811                                TStoredProcedureSqlStatement procedure = (TStoredProcedureSqlStatement) key;
1812                                procedures.add(procedure);
1813                        }
1814                }
1815
1816                return procedures;
1817        }
1818
1819        // public void bindTableByName(TObjectName tableName, Table tableModel) {
1820        // tableNamesMap.put(tableName, tableModel);
1821        //
1822        // }
1823        //
1824        public void bindTableByName(String tableName, Table tableModel) {
1825                bindTableByName((Object) tableName, tableModel);
1826        }
1827
1828        /** Canonical physical-table keys share lifecycle/ordering with legacy bindings. */
1829        public void bindTableByName(Object tableName, Table tableModel) {
1830                if (tableName instanceof TableIdentityKey) {
1831                        TableIdentityKey key = (TableIdentityKey) tableName;
1832                        displaceBinding(tableNamesMap, 0, key.getCanonKey(), tableModel);
1833                        tableNamesMap.put(key.getCanonKey(), tableModel);
1834                        identityBoundTables.add(tableModel);
1835                        // Name-based lookups outside the table identity path (variables, cursors, qualifier
1836                        // strings) still ask by the legacy folded name, so it stays bound as before.
1837                        if (key.getLegacyKey() != null) {
1838                                displaceBinding(tableNamesMap, 0, key.getLegacyKey(), tableModel);
1839                                tableNamesMap.put(key.getLegacyKey(), tableModel);
1840                        }
1841                } else {
1842                        displaceBinding(tableNamesMap, 0, tableName, tableModel);
1843                        tableNamesMap.put(tableName, tableModel);
1844                }
1845                TTable origin = tableModel.getTableObject();
1846                if (origin != null && origin.getResolvedTable() != null && isScopedTemporary(origin)) {
1847                        temporaryMetadataModels.put(origin.getResolvedTable(), tableModel);
1848                        temporaryMetadataOwners.put(origin.getResolvedTable(), getGlobalProcedure());
1849                }
1850
1851        }
1852
1853        // public Table getTableByName(TObjectName tableName) {
1854        // return (Table)tableNamesMap.get(tableName);
1855        // }
1856
1857        /**
1858         * B3 (routine-summary-scc-design §3): the overload pools of the routines whose bodies are being analyzed, active
1859         * ONLY under {@code Option.identityFirstVariablePools}. A pool is keyed by the routine's frozen scope and its
1860         * declaration identity ({@code IdentityScope}), so two same-named overloads keep separate formals and locals; a
1861         * name lookup in a body prefers its pool's variable. Empty in LEGACY by construction. The legacy identity text
1862         * rides along only for the deprecated {@link #peekIdentityScope()}.
1863         */
1864        private final java.util.ArrayDeque<Object[]> poolStack = new java.util.ArrayDeque<Object[]>();
1865
1866        /** @deprecated an overload pool is keyed by its typed scope: use {@link #pushIdentityScope(String, Object)}. */
1867        @Deprecated
1868        public void pushIdentityScope(String identityScopeKey) {
1869                pushIdentityScope(identityScopeKey, null);
1870        }
1871
1872        /**
1873         * Enters an overload's pool: its typed scope ({@code IdentityScope}) and its legacy identity text (metadata). Every
1874         * push is matched by a {@link #popIdentityScope()}; a pool without a typed scope is entered but never active.
1875         */
1876        public void pushIdentityScope(String identityScopeKey, Object poolScope) {
1877                if (identityScopeKey != null || poolScope != null) {
1878                        poolStack.push(new Object[] { poolScope, identityScopeKey });
1879                }
1880        }
1881
1882        public void popIdentityScope() {
1883                if (!poolStack.isEmpty()) {
1884                        poolStack.pop();
1885                }
1886        }
1887
1888        /** The active overload pool's typed scope, or null. */
1889        public Object peekPoolScope() {
1890                Object[] top = poolStack.peek();
1891                return top == null ? null : top[0];
1892        }
1893
1894        public boolean hasIdentityScope() {
1895                return peekPoolScope() != null;
1896        }
1897
1898        /** @deprecated the pool is keyed by {@link #peekPoolScope()}; this is its legacy text, never used to resolve. */
1899        @Deprecated
1900        public String peekIdentityScope() {
1901                Object[] top = poolStack.peek();
1902                return top == null ? null : (String) top[1];
1903        }
1904
1905        /** Indexes a local symbol by its key without listing it again (an alias of a model already listed). */
1906        public void bindLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key, Table symbol) {
1907                if (key != null) {
1908                        localSymbols.put(key, symbol);
1909                }
1910        }
1911
1912        /**
1913         * Binds a local symbol by its key and lists it among the analysis's tables at this point (getTablesByName, and so
1914         * the output order), as every binding of a variable, cursor or correlation name has been listed.
1915         */
1916        public void publishLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key, Table symbol) {
1917                if (key != null) {
1918                        localSymbols.put(key, symbol);
1919                        bindTableByName((Object) key, symbol);
1920                }
1921        }
1922
1923        /**
1924         * The local symbol a reference in {@code stmt} sees under {@code key}'s name: the first scope of the statement's
1925         * lexical chain (routine, enclosing routines, package) that declares it; at top level, the top-level one.
1926         */
1927        public Table lookupVisibleLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key, TCustomSqlStatement stmt) {
1928                return lookupVisibleLocalSymbol(key, stmt, false);
1929        }
1930
1931        /**
1932         * As {@link #lookupVisibleLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey, TCustomSqlStatement)};
1933         * with {@code topLevel}, a declaration at top level is visible from inside a routine too (a declared type).
1934         */
1935        public Table lookupVisibleLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key, TCustomSqlStatement stmt,
1936                        boolean topLevel) {
1937                if (key == null) {
1938                        return null;
1939                }
1940                Object poolScope = peekPoolScope();
1941                java.util.List<Object> chain = gudusoft.gsqlparser.dlineage.symbol.LocalScopes.chainOf(this, stmt);
1942                if (poolScope != null) {
1943                        Table pooled = localSymbols.get(key.inScope(poolScope));
1944                        if (pooled instanceof Variable) {
1945                                return pooled;
1946                        }
1947                        // Every declaration of the overload being analyzed is in its pool: a declared name the pool lacks is not
1948                        // this overload's, and the overload-blind scope of its routine may hold another overload's; so the
1949                        // enclosing routines and the package answer.
1950                        if (poolScope instanceof gudusoft.gsqlparser.dlineage.symbol.IdentityScope
1951                                        && key.getKind().getNamespace() == gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Namespace.DECLARED) {
1952                                chain = new java.util.ArrayList<Object>(chain);
1953                                chain.remove(((gudusoft.gsqlparser.dlineage.symbol.IdentityScope) poolScope).getScope());
1954                        }
1955                }
1956                Table found = firstVisible(localSymbols, key, chain, topLevel);
1957                if (found == null && key.getKind() != gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.CORRELATION) {
1958                        // A bare correlation name (NEW.col) when no declared local of that name is visible.
1959                        found = firstVisible(localSymbols, key.asCorrelation(), chain, topLevel);
1960                }
1961                return found;
1962        }
1963
1964        private static Table firstVisible(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolRegistry symbols,
1965                        gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key, java.util.List<Object> chain, boolean topLevel) {
1966                for (Object scope : chain) {
1967                        Table found = symbols.get(key.inScope(scope));
1968                        if (found != null) {
1969                                return found;
1970                        }
1971                }
1972                return chain.isEmpty() || topLevel ? symbols.get(key.inScope(null)) : null;
1973        }
1974
1975        /** The local symbol visible from the current statement under a name as written (a leading ':' is a sigil). */
1976        public Table visibleLocal(String written, gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind kind) {
1977                if (SQLUtil.isEmpty(written)) {
1978                        return null;
1979                }
1980                boolean correlation = written.startsWith(":");
1981                Stack<TCustomSqlStatement> stack = getGlobalStmtStack();
1982                return lookupVisibleLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.of(null,
1983                                correlation ? written.substring(1) : written,
1984                                correlation ? gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.CORRELATION : kind),
1985                                stack == null || stack.isEmpty() ? null : stack.peek());
1986        }
1987
1988        /**
1989         * A name that may denote a table or a local relation (table variable, cursor, record), by the site's precedence:
1990         * with {@code localFirst} (a %ROWTYPE or %TYPE anchor, a qualifier no FROM item declares) the local relation visible
1991         * from the current statement answers before the table path, as PL/SQL scoping does; otherwise (a FROM or DML
1992         * relation) the table path's answer stands and the local relation answers only when the table path has none.
1993         */
1994        public Table resolveMixedRelation(String written, Table tablePath, boolean localFirst) {
1995                if (tablePath != null && !localFirst) {
1996                        return tablePath;
1997                }
1998                // A local relation: a table variable, cursor or record, or a trigger correlation name (:NEW, the source table).
1999                Table local = visibleLocal(written, gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.TABLE_VARIABLE);
2000                return local != null ? local : tablePath;
2001        }
2002
2003        /** The local symbol under a typed key, or null. */
2004        public Table getLocalSymbol(gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey key) {
2005                return key == null ? null : localSymbols.get(key);
2006        }
2007
2008        /** A canonical miss must not fall back to a case-folded display name. */
2009        public Table getTableByName(Object tableKey) {
2010                if (tableKey instanceof TableIdentityKey) {
2011                        TableIdentityKey key = (TableIdentityKey) tableKey;
2012                        Object bound = tableNamesMap.get(key.getCanonKey());
2013                        if (bound != null) {
2014                                return (Table) bound;
2015                        }
2016                        // The folded name of a table is exactly what merges two tables the analysis keeps
2017                        // apart, so it never answers for one. A binding that was never keyed by table
2018                        // identity (a stage, a stream) is still found by it, as it always was; a local
2019                        // symbol never is: the sites that may name one ask for it explicitly.
2020                        Table byName = key.getLegacyKey() == null ? null : getTableOnly(key.getLegacyKey());
2021                        return byName == null || identityBoundTables.contains(byName) ? null : byName;
2022                }
2023                return tableKey instanceof String ? getTableByName((String) tableKey)
2024                                : (Table) tableNamesMap.get(tableKey);
2025        }
2026
2027        /** A table or other named object bound under a name; never a local symbol. */
2028        private Table getTableOnly(String tableName) {
2029                Table table = (Table) tableNamesMap.get(tableName);
2030                return table != null ? table : (Table) tableNamesMap.get(DlineageUtil.getTableFullName(tableName));
2031        }
2032
2033        /**
2034         * A table or other named object bound under a name; inside a statement, a local symbol visible from it under the
2035         * name as written answers before the name is read again with the session's defaults, as it always has. A name
2036         * composed with a routine's scope ("PROC.@X") no longer names a local symbol: local symbols are keyed by their
2037         * declaring scope and their names as written.
2038         */
2039        public Table getTableByName(String tableName) {
2040                if (tableName == null) {
2041                        return null;
2042                }
2043                Table table = (Table) tableNamesMap.get(tableName);
2044                if (table != null) {
2045                        return table;
2046                }
2047                Stack<TCustomSqlStatement> stmtStack = getGlobalStmtStack();
2048                if (stmtStack != null && !stmtStack.isEmpty()) {
2049                        Table local = visibleLocal(tableName, gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.VARIABLE);
2050                        if (local != null) {
2051                                return local;
2052                        }
2053                        return (Table) tableNamesMap.get(DlineageUtil.getTableFullName(tableName));
2054                }
2055                Table named = (Table) tableNamesMap.get(DlineageUtil.getTableFullName(tableName));
2056                return named != null ? named : visibleLocal(tableName, gudusoft.gsqlparser.dlineage.symbol.LocalSymbolKey.Kind.VARIABLE);
2057        }
2058
2059
2060        /** The routine-registry shadow of this analysis (measurement only; see RoutineShadow). */
2061        public gudusoft.gsqlparser.dlineage.routine.RoutineShadow.State routineShadowState() {
2062                if (routineShadowState == null) {
2063                        routineShadowState = new gudusoft.gsqlparser.dlineage.routine.RoutineShadow.State();
2064                }
2065                return routineShadowState;
2066        }
2067
2068        /**
2069         * The lineage model of a routine statement: the routine model last bound to it, whatever else the statement was
2070         * bound to since. Null when no routine model was bound to it.
2071         */
2072        public Procedure getRoutineModel(TCustomSqlStatement routine) {
2073                return routine == null ? null : routineModels.get(routine);
2074        }
2075
2076        /** Binds a routine model under one of its names as written with one alias suffix. */
2077        public void bindProcedure(RoutineAliasKey key, Procedure procedureModel) {
2078                procedureAliases.put(key, procedureModel);
2079                noteRoutineService(key);
2080        }
2081
2082        private void noteRoutineService(RoutineAliasKey key) {
2083                if (key != null && routineService == null) {
2084                        routineService = getGlobalIdentifierService();
2085                }
2086        }
2087
2088        /**
2089         * Deprecated text read under the rules this manager's routines were keyed by, else under the analysis's on this
2090         * thread: another analysis on the calling thread must not change how this manager reads a name.
2091         */
2092        private RoutineAliasKey legacyTextKey(String text) {
2093                return RoutineAliasKey.ofLegacyText(text, routineService != null ? routineService : getGlobalIdentifierService());
2094        }
2095
2096        /**
2097         * Binds a routine model under a name of the deprecated text-keyed API: the text as given, and the routine name the
2098         * text reads as, so the analysis's own lookups see it.
2099         *
2100         * @deprecated routines are keyed by their names as written: use {@link #bindProcedure(RoutineAliasKey, Procedure)}
2101         */
2102        @Deprecated
2103        public void bindProcedureByName(String procedureName, Procedure procedureModel) {
2104                legacyProcedureNames.put(procedureName, procedureModel);
2105                bindProcedure(legacyTextKey(procedureName), procedureModel);
2106        }
2107
2108        /**
2109         * The routine a name of the deprecated text-keyed API names: looked up as the legacy map was (the enclosing package's
2110         * name prefixed unless the text starts with it; the text as given; then the first entry whose text starts with the
2111         * name and a suffix, or whose last segment matches when one side is qualified), then the routine name the text
2112         * reads as.
2113         *
2114         * @deprecated routines are keyed by their names as written: use {@link #getProcedure(RoutineAliasKey)}
2115         */
2116        @Deprecated
2117        public Procedure getProcedureByName(String procedureName) {
2118                if (procedureName == null) {
2119                        return null;
2120                }
2121                String name = procedureName;
2122                if (getGlobalOraclePackage() != null && !name.startsWith(getGlobalOraclePackage().getName())) {
2123                        name = getGlobalOraclePackage().getName() + "." + name;
2124                }
2125                Procedure procedure = legacyProcedureNames.get(name);
2126                if (procedure == null) {
2127                        boolean suffixed = name.indexOf("(") != -1;
2128                        for (Map.Entry<String, Procedure> entry : legacyProcedureNames.entrySet()) {
2129                                String key = entry.getKey();
2130                                if (key == null) {
2131                                        continue;
2132                                }
2133                                if (!suffixed) {
2134                                        if (key.startsWith(name + "(")) {
2135                                                return entry.getValue();
2136                                        }
2137                                        if (key.indexOf(".") != -1 && name.indexOf(".") == -1
2138                                                        && key.substring(key.lastIndexOf(".") + 1).startsWith(name + "(")) {
2139                                                return entry.getValue();
2140                                        }
2141                                        if (key.indexOf(".") == -1 && name.indexOf(".") != -1
2142                                                        && key.startsWith(name.substring(name.lastIndexOf(".") + 1) + "(")) {
2143                                                return entry.getValue();
2144                                        }
2145                                } else {
2146                                        if (key.startsWith(name)) {
2147                                                return entry.getValue();
2148                                        }
2149                                        if (key.indexOf(".") != -1 && name.indexOf(".") == -1 && sameStoredBindingName(
2150                                                        ESQLDataObjectType.dotProcedure, key.substring(key.lastIndexOf(".") + 1), name)) {
2151                                                return entry.getValue();
2152                                        }
2153                                        if (key.indexOf(".") == -1 && name.indexOf(".") != -1 && sameStoredBindingName(
2154                                                        ESQLDataObjectType.dotProcedure, key, name.substring(name.lastIndexOf(".") + 1))) {
2155                                                return entry.getValue();
2156                                        }
2157                                }
2158                        }
2159                        procedure = getProcedure(legacyTextKey(procedureName));
2160                }
2161                return procedure;
2162        }
2163
2164        /** The routine a name as written refers to, by the legacy index's precedence (RoutineAliasRegistry.lookup). */
2165        public Procedure getProcedure(RoutineAliasKey key) {
2166                OraclePackage enclosing = getGlobalOraclePackage();
2167                return procedureAliases.lookup(key, enclosing == null ? null : enclosing.getAliasKey());
2168        }
2169
2170        public void bindOraclePackage(RoutineAliasKey key, OraclePackage oraclePackageModel) {
2171                packageAliases.put(key, oraclePackageModel);
2172                noteRoutineService(key);
2173        }
2174
2175        public OraclePackage getOraclePackage(RoutineAliasKey key) {
2176                return packageAliases.get(key);
2177        }
2178
2179        /**
2180         * Binds an Oracle package under a name of the deprecated text-keyed API: the text as given, and the package name
2181         * the text reads as.
2182         *
2183         * @deprecated packages are keyed by their names as written: use {@link #bindOraclePackage(RoutineAliasKey, OraclePackage)}
2184         */
2185        @Deprecated
2186        public void bindOraclePackageByName(String oraclePackageName, OraclePackage oraclePackageModel) {
2187                legacyPackageNames.put(oraclePackageName, oraclePackageModel);
2188                bindOraclePackage(legacyTextKey(oraclePackageName), oraclePackageModel);
2189        }
2190
2191        /**
2192         * The Oracle package under a name of the deprecated text-keyed API: the text as given, then the package name the
2193         * text reads as.
2194         *
2195         * @deprecated packages are keyed by their names as written: use {@link #getOraclePackage(RoutineAliasKey)}
2196         */
2197        @Deprecated
2198        public OraclePackage getOraclePackageByName(String oraclePackageName) {
2199                OraclePackage oraclePackage = legacyPackageNames.get(oraclePackageName);
2200                return oraclePackage != null ? oraclePackage : getOraclePackage(legacyTextKey(oraclePackageName));
2201        }
2202
2203
2204        public String getSqlHash(TCustomSqlStatement stmt) {
2205                return topStmtHashMap.get(stmt);
2206        }
2207
2208        public Map getHashSQLMap() {
2209                return hashSQLMap;
2210        }
2211
2212        public Map getDynamicSQLMap() {
2213                return dynamicSQLMap;
2214        }
2215
2216        public void collectSqlHash(TCustomSqlStatement stmt) {
2217                if (getGlobalOption().getAnalyzeMode() == AnalyzeMode.crud) {
2218                        String sql = stmt.toString();
2219                        if (sql != null) {
2220                                sql = stmt.getStartToken().lineNo + ":" + sql;
2221                                String hash = SQLUtil.stringToMD5(sql);
2222                                topStmtHashMap.put(stmt, hash);
2223                                hashSQLMap.put(hash, new Pair3<Long, Long, String>(stmt.getStartToken().lineNo, stmt.getEndToken().lineNo, ModelBindingManager.getGlobalHash()));
2224                        }
2225                } else {
2226                        try {
2227                                String sql = stmt.asCanonical();
2228                                if (sql != null) {
2229                                        String hash = SQLUtil.stringToMD5(sql);
2230                                        topStmtHashMap.put(stmt, hash);
2231                                        hashSQLMap.put(hash, sql);
2232                                }
2233                        } catch (Exception e) {
2234                                logger.error("collectSqlHash error: " + e.getMessage(), e);
2235                        }
2236                }
2237        }
2238        
2239        public void collectDynamicSqlHash(TCustomSqlStatement stmt) {
2240                if (getGlobalOption().getAnalyzeMode() == AnalyzeMode.dynamic) {
2241                        String sql = stmt.toString();
2242                        if (sql != null) {
2243                                sql = stmt.getStartToken().lineNo + ":" + sql;
2244                                String hash = SQLUtil.stringToMD5(sql);
2245                                dynamicSQLMap.put(hash, new Pair3<Long, Long, String>(stmt.getStartToken().lineNo, stmt.getEndToken().lineNo, ModelBindingManager.getGlobalHash()));
2246                        }
2247                }
2248        }
2249
2250        public void appendDblinkTable(String name) {
2251                if (!SQLUtil.isEmpty(name)) {
2252                        dblinkTableSet.add(name);
2253                }
2254        }
2255        
2256        public boolean isDblinkTable(String name) {
2257                return dblinkTableSet.contains(name);
2258        }
2259
2260        public Set<ResultSet> getResultSets() {
2261                return resultSetSet;
2262        }
2263        
2264        public void dropTable(Table table) {
2265                List tableNames = new ArrayList(tableNamesMap.keySet());
2266                for (Object tableName : tableNames) {
2267                        if(tableNamesMap.get(tableName) == table) {
2268                                tableNamesMap.remove(tableName);
2269                                dropTables.add(table);
2270                        }
2271                }
2272        }
2273        
2274        public void cleanTempTable() {
2275                Iterator<Object> iter = tableNamesMap.keySet().iterator();
2276                while(iter.hasNext()) {
2277                        Object tableName = iter.next();
2278                        if(DlineageUtil.isTempTable((Table)tableNamesMap.get(tableName), getGlobalVendor())) {
2279                                dropTables.add((Table)tableNamesMap.get(tableName));
2280                                iter.remove();
2281                        }
2282                }
2283        }
2284
2285        /**
2286         * Save session/caller bindings while analyzing a separate routine body. The first two maps
2287         * hold the temporary tables visible on entry; the last two, filled while the body is analyzed,
2288         * hold the shared tables its own temporary tables displaced by name.
2289         */
2290        public List<Map> enterTemporaryTableScope() {
2291                List<Map> saved = new ArrayList<Map>();
2292                for (Map bindings : Arrays.asList(tableNamesMap, createModelQuickBindingMap)) {
2293                        Map snapshot = new LinkedHashMap();
2294                        for (Object key : bindings.keySet()) {
2295                                if (isScopedTemporaryModel(bindings.get(key))) snapshot.put(key, bindings.get(key));
2296                        }
2297                        saved.add(snapshot);
2298                }
2299                saved.add(new LinkedHashMap());
2300                saved.add(new LinkedHashMap());
2301                temporaryScopes.push(saved);
2302                return saved;
2303        }
2304
2305        /** Local declarations remain in the output, but cannot become another routine's metadata. */
2306        public void leaveTemporaryTableScope(List<Map> saved) {
2307                for (Iterator<List<Map>> scopes = temporaryScopes.iterator(); scopes.hasNext();) {
2308                        if (scopes.next() == saved) {
2309                                scopes.remove();
2310                                break;
2311                        }
2312                }
2313                int index = 0;
2314                for (Map bindings : Arrays.asList(tableNamesMap, createModelQuickBindingMap)) {
2315                        Iterator iterator = bindings.values().iterator();
2316                        while (iterator.hasNext()) {
2317                                if (isScopedTemporaryModel(iterator.next())) iterator.remove();
2318                        }
2319                        bindings.putAll(saved.get(index));
2320                        // A temporary table named like a shared one (CREATE TEMP TABLE t, with a permanent t)
2321                        // hid it only while this body was analyzed.
2322                        for (Object entry : saved.get(index + 2).entrySet()) {
2323                                Map.Entry displaced = (Map.Entry) entry;
2324                                if (!bindings.containsKey(displaced.getKey())) bindings.put(displaced.getKey(), displaced.getValue());
2325                        }
2326                        index++;
2327                }
2328        }
2329
2330        /** Remember a shared binding that a routine body's temporary table is about to hide. */
2331        private void displaceBinding(Map bindings, int index, Object key, Object model) {
2332                if (temporaryScopes.isEmpty() || !isScopedTemporaryModel(model)) return;
2333                Object previous = bindings.get(key);
2334                if (previous == null || previous == model || isScopedTemporaryModel(previous)) return;
2335                Map displaced = temporaryScopes.peek().get(index + 2);
2336                if (!displaced.containsKey(key)) displaced.put(key, previous);
2337        }
2338
2339        private boolean isScopedTemporaryModel(Object model) {
2340                return model instanceof Table && isScopedTemporary(((Table) model).getTableObject());
2341        }
2342
2343        public Set<Table> getDropTables() {
2344                return dropTables;
2345        }
2346
2347        // --- Pipelined function stitching methods ---
2348
2349        /**
2350         * Indexes an object type's fields under a key of the deprecated text API.
2351         *
2352         * @deprecated types are keyed by their names as written: use {@link #indexObjectTypeFields(Object, List)}
2353         */
2354        @Deprecated
2355        public void indexObjectType(String normalizedName, List<String> fieldNames) {
2356                objectTypeIndex.put(normalizedName, fieldNames);
2357        }
2358
2359        /** @deprecated use {@link #getObjectTypeFieldsByKey(Object)} */
2360        @Deprecated
2361        public List<String> getObjectTypeFields(String normalizedName) {
2362                return objectTypeIndex.get(normalizedName);
2363        }
2364
2365        /** @deprecated use {@link #indexCollectionElementType(Object, String)} */
2366        @Deprecated
2367        public void indexCollectionType(String collectionName, String elementTypeName) {
2368                collectionToElementTypeIndex.put(collectionName, elementTypeName);
2369        }
2370
2371        /** @deprecated use {@link #getCollectionElementType(Object)} */
2372        @Deprecated
2373        public String getElementTypeName(String collectionName) {
2374                return collectionToElementTypeIndex.get(collectionName);
2375        }
2376
2377        /** Indexes an object type's fields under the identity of its name. */
2378        public void indexObjectTypeFields(Object typeKey, List<String> fieldNames) {
2379                if (typeKey != null) {
2380                        objectTypeIndex.put(typeKey, fieldNames);
2381                }
2382        }
2383
2384        /** The fields of the object type a key names, or null. */
2385        public List<String> getObjectTypeFieldsByKey(Object typeKey) {
2386                return typeKey == null ? null : objectTypeIndex.get(typeKey);
2387        }
2388
2389        /** Indexes a collection type's element type, the element type's name as written, under the collection's key. */
2390        public void indexCollectionElementType(Object collectionKey, String elementTypeName) {
2391                if (collectionKey != null) {
2392                        collectionToElementTypeIndex.put(collectionKey, elementTypeName);
2393                }
2394        }
2395
2396        /** The element type name, as written, of the collection type a key names, or null. */
2397        public String getCollectionElementType(Object collectionKey) {
2398                return collectionKey == null ? null : collectionToElementTypeIndex.get(collectionKey);
2399        }
2400
2401        /**
2402         * Registers a pipelined signature under its function's identity and the identity of its bare name; a signature
2403         * built through the deprecated text keys is registered under those.
2404         */
2405        public void addPipelinedSignature(PipelinedFunctionSignature sig) {
2406                Object key = sig.getFunctionIdentity() != null ? sig.getFunctionIdentity() : sig.getFunctionKey();
2407                Object naked = sig.getNakedIdentity() != null ? sig.getNakedIdentity() : sig.getNakedKey();
2408                pipelinedSignatures.computeIfAbsent(key, k -> new ArrayList<>()).add(sig);
2409                if (naked != null && !naked.equals(key)) {
2410                        pipelinedSignatures.computeIfAbsent(naked, k -> new ArrayList<>()).add(sig);
2411                }
2412        }
2413
2414        /**
2415         * The signature a call names under keys of the deprecated text API.
2416         *
2417         * @deprecated signatures are registered under the identities of their names: use
2418         *             {@link #findPipelinedSignatureByKeys(List, int)}
2419         */
2420        @Deprecated
2421        public PipelinedFunctionSignature findPipelinedSignature(List<String> candidates, int argCount) {
2422                return findPipelinedSignatureByKeys(candidates, argCount);
2423        }
2424
2425        /**
2426         * The one signature registered under the first of {@code keys} that has any accepting {@code argCount} (any count
2427         * when negative); null when none, or when that key is ambiguous.
2428         */
2429        public PipelinedFunctionSignature findPipelinedSignatureByKeys(List<?> keys, int argCount) {
2430                for (Object key : keys) {
2431                        List<PipelinedFunctionSignature> sigs = pipelinedSignatures.get(key);
2432                        if (sigs != null) {
2433                                PipelinedFunctionSignature match = null;
2434                                for (PipelinedFunctionSignature sig : sigs) {
2435                                        if (sig.getArgCount() == argCount || argCount < 0) {
2436                                                if (match != null) {
2437                                                        return null; // ambiguous
2438                                                }
2439                                                match = sig;
2440                                        }
2441                                }
2442                                if (match != null) {
2443                                        return match;
2444                                }
2445                        }
2446                }
2447                return null;
2448        }
2449
2450        /**
2451         * @deprecated signatures are registered under the identities of their names: use
2452         *             {@link #findPipelinedSignatureAnyByKeys(List)}
2453         */
2454        @Deprecated
2455        public PipelinedFunctionSignature findPipelinedSignatureAny(List<String> candidates) {
2456                return findPipelinedSignatureAnyByKeys(candidates);
2457        }
2458
2459        /** The only signature registered under the first of {@code keys} that has exactly one; null when none. */
2460        public PipelinedFunctionSignature findPipelinedSignatureAnyByKeys(List<?> keys) {
2461                for (Object key : keys) {
2462                        List<PipelinedFunctionSignature> sigs = pipelinedSignatures.get(key);
2463                        if (sigs != null && !sigs.isEmpty()) {
2464                                if (sigs.size() == 1) {
2465                                        return sigs.get(0);
2466                                }
2467                        }
2468                }
2469                return null;
2470        }
2471
2472        public void addPendingPipelinedCallSite(PendingPipelinedCallSite callSite) {
2473                pendingPipelinedCallSites.add(callSite);
2474        }
2475
2476        public List<PendingPipelinedCallSite> getPendingPipelinedCallSites() {
2477                return pendingPipelinedCallSites;
2478        }
2479
2480        public Set<String> getResolvingPipelinedFunctions() {
2481                return resolvingPipelinedFunctions;
2482        }
2483
2484        /**
2485         * The registered signatures under their names as written, the function's name and then its bare name: a view for
2486         * the deprecated text API (the registry is keyed by the names' identities).
2487         *
2488         * @deprecated use {@link #getPipelinedSignaturesByKey()}
2489         */
2490        @Deprecated
2491        public Map<String, List<PipelinedFunctionSignature>> getPipelinedSignatures() {
2492                Map<String, List<PipelinedFunctionSignature>> byName = new LinkedHashMap<>();
2493                Set<PipelinedFunctionSignature> listed = Collections.newSetFromMap(
2494                                new IdentityHashMap<PipelinedFunctionSignature, Boolean>());
2495                for (List<PipelinedFunctionSignature> signatures : pipelinedSignatures.values()) {
2496                        for (PipelinedFunctionSignature sig : signatures) {
2497                                if (!listed.add(sig)) {
2498                                        continue;
2499                                }
2500                                byName.computeIfAbsent(sig.getFunctionKey(), k -> new ArrayList<>()).add(sig);
2501                                if (sig.getNakedKey() != null && !sig.getNakedKey().equals(sig.getFunctionKey())) {
2502                                        byName.computeIfAbsent(sig.getNakedKey(), k -> new ArrayList<>()).add(sig);
2503                                }
2504                        }
2505                }
2506                return byName;
2507        }
2508
2509        /** The pipelined signature registry: signatures under the identities of their names (and deprecated text keys). */
2510        public Map<Object, List<PipelinedFunctionSignature>> getPipelinedSignaturesByKey() {
2511                return pipelinedSignatures;
2512        }
2513
2514}