001package gudusoft.gsqlparser.resolver2.metadata;
002
003import gudusoft.gsqlparser.*;
004import gudusoft.gsqlparser.nodes.*;
005import gudusoft.gsqlparser.resolver.TMetadataCollector;
006import gudusoft.gsqlparser.resolver2.context.DatabaseContextTracker;
007import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver;
008import gudusoft.gsqlparser.resolver2.namespace.TableNamespace;
009import gudusoft.gsqlparser.sqlenv.*;
010import gudusoft.gsqlparser.stmt.TCallStatement;
011import gudusoft.gsqlparser.stmt.TCreateFunctionStmt;
012import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
013import gudusoft.gsqlparser.stmt.TDropTableSqlStatement;
014import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement;
015import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
016import gudusoft.gsqlparser.stmt.mssql.TMssqlExecute;
017import gudusoft.gsqlparser.util.SQLUtil;
018import java.util.*;
019
020/** Captures table metadata and lexical defaults at each statement's position. */
021public final class StatementMetadata {
022    /** Distinguishes a routine-local catalog from a separately parsed batch's environment. */
023    public static final class ProcedureEnvironment extends TSQLEnv {
024        private ProcedureEnvironment(EDbVendor vendor, IdentifierProfile profile) {
025            super(vendor, IdentifierProfile.forVendorOr(profile, vendor));
026        }
027        @Override public void initSQLEnv() { }
028    }
029    public static final class Context {
030        public final String catalog;
031        public final String schema;
032        private Context(String catalog, String schema) {
033            this.catalog = catalog;
034            this.schema = schema;
035        }
036    }
037
038    /**
039     * A temporary-table fact in a routine body, or in the top-level script, in statement order:
040     * a transient table is defined or dropped, a routine is called, or a table is referenced.
041     */
042    private static final class TemporaryEvent {
043        enum Kind { DEFINE, DROP, CALL, REFERENCE }
044        final Kind kind;
045        final int sequence;
046        /** The routine whose body holds the event; null for the top-level script. */
047        final TStoredProcedureSqlStatement body;
048        final List<CanonKey> key;
049        final TSQLTable table;
050        final TemporaryTableSemantics.Scope scope;
051        final TTable reference;
052        TStoredProcedureSqlStatement target;
053        Map<List<CanonKey>, Object> visibleAtCall;
054
055        private TemporaryEvent(Kind kind, int sequence, TStoredProcedureSqlStatement body, List<CanonKey> key,
056                TSQLTable table, TemporaryTableSemantics.Scope scope, TTable reference) {
057            this.kind = kind;
058            this.sequence = sequence;
059            this.body = body;
060            this.key = key;
061            this.table = table;
062            this.scope = scope;
063            this.reference = reference;
064        }
065    }
066
067    /** In a map of visible temporary tables: some temporary table is visible, but which one depends on the path. */
068    private static final Object CONFLICT = new Object();
069
070    private final Map<TTable, TSQLTable> tables = new IdentityHashMap<>();
071    private final Map<TTable, Context> tableContexts = new IdentityHashMap<>();
072    private final Map<TCustomSqlStatement, Context> contexts = new LinkedHashMap<>();
073    private final Map<TStoredProcedureSqlStatement, TSQLEnv> procedureEnvironments = new IdentityHashMap<>();
074    private final Map<TStoredProcedureSqlStatement, List<TemporaryEvent>> routineEvents = new IdentityHashMap<>();
075    private final List<TemporaryEvent> scriptEvents = new ArrayList<>();
076    private final Set<TSQLTable> transientTables = Collections.newSetFromMap(new IdentityHashMap<TSQLTable, Boolean>());
077    private int eventSequence;
078
079    public Map<TCustomSqlStatement, Context> getContexts() { return contexts; }
080
081    /** Register a function using its declaration's lexical defaults, not the batch's defaults. */
082    public void registerFunction(TCreateFunctionStmt statement, TSQLEnv env) {
083        Context context = contexts.get(statement);
084        if (context == null) {
085            registerReturnColumns(env.addFunction(statement.getFunctionName(), true), statement);
086            return;
087        }
088        String savedCatalog = env.getDefaultCatalogName();
089        String savedSchema = env.getDefaultSchemaName();
090        try {
091            env.setDefaultCatalogName(context.catalog);
092            env.setDefaultSchemaName(context.schema);
093            registerReturnColumns(env.addFunction(statement.getFunctionName(), true), statement);
094        } finally {
095            env.setDefaultCatalogName(savedCatalog);
096            env.setDefaultSchemaName(savedSchema);
097        }
098    }
099
100    /**
101     * The columns a function's declaration gives its table result (TableFunctionLayout.declaredColumns), registered
102     * with it: a call of it in FROM has them.
103     */
104    public static void registerReturnColumns(gudusoft.gsqlparser.sqlenv.TSQLFunction function,
105            TCreateFunctionStmt statement) {
106        if (function != null && function.getReturnType() == null && statement.getReturnDataType() != null) {
107            function.setReturnType(statement.getReturnDataType()); // a call reads its row type's columns (slice 13f)
108        }
109        java.util.List<String> columns = function == null ? null
110                : gudusoft.gsqlparser.resolver2.namespace.TableFunctionLayout.declaredColumns(statement);
111        if (columns != null) {
112            for (String column : columns) {
113                function.addReturnColumn(column);
114            }
115        }
116    }
117
118    public void configure(TableNamespace namespace, TTable table) {
119        Context context = tableContexts.get(table);
120        if (context != null) {
121            TSQLTable metadata = tables.get(table);
122            // A SELECT INTO * target has identity before its projection has a known shape.
123            if (metadata != null && metadata.getColumnList().isEmpty()
124                    && (transientTables.contains(metadata)
125                    || SQLUtil.isLocalTemporaryTable(table.dbvendor, table.getTableName()))) metadata = null;
126            namespace.setStatementMetadata(metadata, new QualifiedNameResolver(
127                    context.catalog, context.schema, table.dbvendor));
128        }
129    }
130
131    public void collect(TStatementList statements, TSQLEnv env) {
132        String savedCatalog = env.getDefaultCatalogName();
133        String savedSchema = env.getDefaultSchemaName();
134        DatabaseContextTracker tracker = new DatabaseContextTracker(savedCatalog, savedSchema);
135        Set<TCustomSqlStatement> seen = Collections.newSetFromMap(new IdentityHashMap<TCustomSqlStatement, Boolean>());
136        Deque<TCustomSqlStatement> pending = new ArrayDeque<>();
137        pushStatements(pending, statements);
138        try {
139            while (!pending.isEmpty()) {
140                TCustomSqlStatement stmt = pending.pop();
141                if (!seen.add(stmt)) continue;
142                tracker.processStatement(stmt);
143                Context context = contextOf(tracker, stmt.dbvendor, env);
144                String catalog = context.catalog;
145                String schema = context.schema;
146                env.setDefaultCatalogName(catalog);
147                env.setDefaultSchemaName(schema);
148                contexts.put(stmt, context);
149                boolean temporary = TemporaryTableSemantics.hasTransientTables(stmt.dbvendor);
150                TStoredProcedureSqlStatement body = temporary ? procedureOwner(stmt) : null;
151                TSQLEnv localEnv = body == null ? null : procedureEnvironments.get(body);
152                if (localEnv != null) {
153                    localEnv.setDefaultCatalogName(catalog);
154                    localEnv.setDefaultSchemaName(schema);
155                }
156                if (stmt instanceof TCreateTableSqlStatement) {
157                    declareTable((TCreateTableSqlStatement) stmt, context, body, env, temporary);
158                } else if (stmt instanceof gudusoft.gsqlparser.stmt.TAlterTableStatement) {
159                    addColumns((gudusoft.gsqlparser.stmt.TAlterTableStatement) stmt, env);
160                }
161                for (int i = 0; i < stmt.tables.size(); i++) {
162                    TTable table = stmt.tables.getTable(i);
163                    if (table.getTableName() == null) continue;
164                    if (temporary && table.getEffectType() == ETableEffectType.tetSelectInto
165                            && stmt instanceof TSelectSqlStatement) {
166                        declareSelectInto((TSelectSqlStatement) stmt, table, context, body, env);
167                    }
168                    tableContexts.put(table, context);
169                    bindTable(table, context, body, env, temporary);
170                }
171                if (temporary) {
172                    TObjectName called = calledRoutine(stmt);
173                    if (called != null) {
174                        record(new TemporaryEvent(TemporaryEvent.Kind.CALL, eventSequence++, body,
175                                procedureKey(called, context, env), null, null, null));
176                    }
177                    if (stmt instanceof TDropTableSqlStatement) dropTables((TDropTableSqlStatement) stmt, context, body, env);
178                }
179                pushStatements(pending, stmt.getStatements());
180            }
181            bindTemporaryTables(env);
182        } finally {
183            env.setDefaultCatalogName(savedCatalog);
184            env.setDefaultSchemaName(savedSchema);
185        }
186    }
187
188    /**
189     * The database and schema a statement runs in, from the {@code USE} statements before it as {@code tracker}
190     * replayed them: Snowflake's schema after a database switch when the catalog proves one, SQL Server's dbo.
191     */
192    static Context contextOf(DatabaseContextTracker tracker, EDbVendor vendor, TSQLEnv env) {
193        String catalog = tracker.getCurrentDatabase();
194        String schema = tracker.getCurrentSchemaComponent();
195        if (schema == null && vendor == EDbVendor.dbvsnowflake && catalog != null) {
196            schema = env.findSchemaAfterDatabaseSwitch(catalog);
197        }
198        // SQL Server unqualified objects live in dbo unless the caller supplies a schema.
199        if (schema == null && (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql)) {
200            schema = "dbo";
201        }
202        return new Context(catalog, schema);
203    }
204
205    /**
206     * A CREATE TABLE. A routine's transient table goes to the routine's own catalog; anything
207     * else, a top-level temporary table included, to the shared one, as before.
208     */
209    private void declareTable(TCreateTableSqlStatement create, Context context, TStoredProcedureSqlStatement body,
210            TSQLEnv env, boolean temporary) {
211        TemporaryTableSemantics.Scope scope = temporary ? TemporaryTableSemantics.ofDeclaration(create) : null;
212        if (!TemporaryTableSemantics.isTransient(scope)) {
213            TMetadataCollector.registerDeclaredTable(create, env);
214            return;
215        }
216        TSQLEnv target = body != null ? procedureEnvironment(body, context, env) : env;
217        TSQLTable declared = TMetadataCollector.registerDeclaredTable(create, target);
218        if (declared == null) declared = declareTransientQuery(create, target, body != null);
219        if (declared != null) define(declared, create.getTableName(), create.dbvendor, context, body, env, scope);
220    }
221
222    /**
223     * ALTER TABLE ... ADD COLUMN(S): the statements after it read the table with the columns added, each with its
224     * type, a STRUCT column with its fields (Mantis 4846). The catalog gets a new version of the table, and a
225     * statement before the ALTER keeps the one it found: there the name of an added column still means what it meant.
226     * A table the catalog does not hold is left alone: nothing proves which other columns it has. So is an ALTER inside
227     * a routine, which runs when the routine is called, not where it is declared, and a sealed table.
228     */
229    private static void addColumns(gudusoft.gsqlparser.stmt.TAlterTableStatement alter, TSQLEnv env) {
230        if (!env.isEnableGetMetadataFromDDL() || (alter.getParentStmt() != null) || (alter.getTableName() == null)
231                || (alter.getAlterTableOptionList() == null)) {
232            return;
233        }
234        TSQLTable found = env.searchTable(alter.getTableName());
235        if ((found == null) || found.isSealed() || (found.getSchema() == null) || found.getSchema().isSealed()) return;
236        TSQLTable table = found;
237        for (TAlterTableOption option : alter.getAlterTableOptionList()) {
238            if (((option.getOptionType() != EAlterTableOptionType.AddColumn)
239                    && (option.getOptionType() != EAlterTableOptionType.addColumnIfNotExists))
240                    || (option.getColumnDefinitionList() == null)) {
241                continue;
242            }
243            for (TColumnDefinition column : option.getColumnDefinitionList()) {
244                if ((column.getColumnName() == null) || (table.getColumn(column.getColumnName().toString()) != null)) {
245                    continue;
246                }
247                if (table == found) table = found.newVersion();
248                table.addColumn(column.getColumnName().toString(), column.getDatatype());
249            }
250        }
251    }
252
253    /**
254     * The table a transient table's declaration with no column list gives {@code target} (its routine's own catalog,
255     * or the shared one for a top-level table), or null. Shared with the parse ({@link ParseTimeCatalog}).
256     */
257    static TSQLTable declareTransientQuery(TCreateTableSqlStatement create, TSQLEnv target, boolean inRoutine) {
258        // CREATE TEMP TABLE ... AS SELECT takes the query's projection; any other form (LIKE,
259        // a copy) still declares a table of its own, whose shape is not known.
260        if (inRoutine) return declareQueryTable(target, create.getTableName(), create.getSubQuery());
261        // A top-level table stays in the shared catalog, where the whole-script DDL preload may
262        // already have put it (CREATE TEMP TABLE ... AS SELECT); routines see it there.
263        // A #name is never a catalog table's, so its query shape cannot be taken for one:
264        // register it here too (a dynamic statement's CREATE TABLE #t AS SELECT is never preloaded).
265        if (TemporaryTableSemantics.ofName(create.dbvendor, create.getTableName()) == null) return null;
266        return declareQueryTable(target, create.getTableName(), create.getSubQuery());
267    }
268
269    /** A {@code SELECT ... INTO} target that creates a transient table ({@code INTO #t}, {@code INTO TEMP t}). */
270    private void declareSelectInto(TSelectSqlStatement select, TTable table, Context context,
271            TStoredProcedureSqlStatement body, TSQLEnv env) {
272        TemporaryTableSemantics.Scope scope = TemporaryTableSemantics.ofSelectInto(select.dbvendor, table.getTableName());
273        if (!TemporaryTableSemantics.isTransient(scope)) return;
274        // Outside a routine TIntoClause has already registered the target in the shared catalog.
275        TSQLTable declared = body != null
276                ? declareQueryTable(procedureEnvironment(body, context, env), table.getTableName(), select)
277                : env.searchTable(table.getTableName());
278        if (declared != null) define(declared, table.getTableName(), select.dbvendor, context, body, env, scope);
279    }
280
281    static TSQLTable declareQueryTable(TSQLEnv target, TObjectName name, TSelectSqlStatement query) {
282        TSQLTable declared = target.addTable(name.toString(), true);
283        if (declared != null) addProjection(declared, query);
284        return declared;
285    }
286
287    /**
288     * {@link #declareQueryTable(TSQLEnv, TObjectName, TSelectSqlStatement)} where an unqualified name is in
289     * {@code catalog} and {@code schema}, leaving {@code target}'s defaults as they are. With {@code replace} the
290     * table is a new one in place of a table {@code target} holds there.
291     */
292    static TSQLTable declareQueryTable(TSQLEnv target, TObjectName name, TSelectSqlStatement query, String catalog,
293            String schema, boolean replace) {
294        if (!target.isEnableGetMetadataFromDDL()) return null;
295        TSQLSchema owner = TMetadataCollector.declaredTableOwner(name, target.getDBVendor(), target, catalog, schema);
296        TSQLTable declared = replace ? new TSQLTable(owner, name.getObjectString())
297                : owner.createTable(name.getObjectString());
298        if (declared != null) addProjection(declared, query);
299        return declared;
300    }
301
302    /** The columns {@code query} gives a table it fills: those of its first SELECT. */
303    private static void addProjection(TSQLTable declared, TSelectSqlStatement query) {
304        if (query == null) return;
305        TSelectSqlStatement projection = query.isCombinedQuery() ? query.getLeftStmt() : query;
306        while (projection != null && projection.isCombinedQuery()) projection = projection.getLeftStmt();
307        if (projection == null || projection.getResultColumnList() == null) return;
308        for (TResultColumn column : projection.getResultColumnList()) {
309            TObjectName field = column.getFieldAttr();
310            // SELECT * has no shape until the star is expanded; leave the table open.
311            if (field != null && field.getPartToken() != null
312                    && field.getPartToken().tokentype == ETokenType.ttasterisk) return;
313        }
314        projection.addColumnInSelectListToSQLEnv(declared);
315    }
316
317    private void define(TSQLTable declared, TObjectName name, EDbVendor vendor, Context context,
318            TStoredProcedureSqlStatement body, TSQLEnv env, TemporaryTableSemantics.Scope scope) {
319        transientTables.add(declared);
320        record(new TemporaryEvent(TemporaryEvent.Kind.DEFINE, eventSequence++, body,
321                temporaryKey(name, vendor, context, env), declared, scope, null));
322    }
323
324    private void dropTables(TDropTableSqlStatement drop, Context context, TStoredProcedureSqlStatement body, TSQLEnv env) {
325        List<TObjectName> names = new ArrayList<>();
326        if (drop.getTableNameList() != null) {
327            for (int i = 0; i < drop.getTableNameList().size(); i++) names.add(drop.getTableNameList().getObjectName(i));
328        } else if (drop.getTableName() != null) {
329            names.add(drop.getTableName());
330        }
331        for (TObjectName name : names) {
332            if (name == null) continue;
333            record(new TemporaryEvent(TemporaryEvent.Kind.DROP, eventSequence++, body,
334                    temporaryKey(name, drop.dbvendor, context, env), null, null, null));
335        }
336    }
337
338    /**
339     * Bind a reference at its position: a transient table its own body defined so far shadows a
340     * catalog table of the same name. Calls, drops and callers are applied after the walk.
341     */
342    private void bindTable(TTable table, Context context, TStoredProcedureSqlStatement body, TSQLEnv env,
343            boolean temporary) {
344        boolean namedTemporary = SQLUtil.isLocalTemporaryTable(table.dbvendor, table.getTableName());
345        // A #name table in a routine is the routine's, even before a declaration binds it.
346        TSQLEnv localEnv = body == null ? null
347                : namedTemporary ? procedureEnvironment(body, context, env) : procedureEnvironments.get(body);
348        // Only transient definitions of this body are stored in its own catalog.
349        TSQLTable metadata = localEnv != null ? localEnv.searchTable(table.getTableName()) : null;
350        if (localEnv != null && namedTemporary) table.setSqlEnv(localEnv);
351        if (metadata != null) table.setSqlEnv(metadata.getSqlEnv());
352        if (metadata == null) metadata = env.searchTable(table.getTableName());
353        if (metadata != null && localEnv != null && namedTemporary) table.setSqlEnv(metadata.getSqlEnv());
354        tables.put(table, metadata);
355        table.setResolvedTable(metadata);
356        table.setCatalogStatus(catalogStatus(table, metadata, env, namedTemporary ? localEnv : null));
357        if (temporary && table.getTableType() == ETableSource.objectname && !table.isCTEName()) {
358            record(new TemporaryEvent(TemporaryEvent.Kind.REFERENCE, eventSequence++, body,
359                    temporaryKey(table.getTableName(), table.dbvendor, context, env), null, null, table));
360        }
361    }
362
363    /**
364     * The table-to-catalog fact, kept apart from the column-to-table fact. NOT_FOUND is claimed
365     * only for a base table reference, and only when there was metadata to look in.
366     */
367    private static ETableCatalogStatus catalogStatus(TTable table, TSQLTable metadata, TSQLEnv env, TSQLEnv localEnv) {
368        if (metadata != null) return ETableCatalogStatus.FOUND;
369        if (table.getTableType() != ETableSource.objectname || table.isCTEName()) {
370            return ETableCatalogStatus.NOT_CHECKED;
371        }
372        TObjectName name = table.getTableName();
373        // Decided from the parse, never from the spelling: a table variable (@tv) is not a
374        // catalog object, and for a reference through a database link the local catalog is not
375        // the authority.
376        if (name != null && ((name.getEndToken() != null && name.getEndToken().tokentype == ETokenType.ttsqlvar)
377                || name.getDblink() != null)) {
378            return ETableCatalogStatus.NOT_CHECKED;
379        }
380        boolean anyMetadata = (env != null && env.hasCatalogMetadata())
381                || (localEnv != null && localEnv.hasCatalogMetadata());
382        return anyMetadata ? ETableCatalogStatus.NOT_FOUND : ETableCatalogStatus.NOT_CHECKED;
383    }
384
385    /** The routine's own catalog, made when the routine first needs one, with the statement's defaults. */
386    private TSQLEnv procedureEnvironment(TStoredProcedureSqlStatement procedure, Context context, TSQLEnv env) {
387        TSQLEnv local = procedureEnvironments.get(procedure);
388        if (local == null) {
389            local = newProcedureEnvironment(env, context);
390            procedureEnvironments.put(procedure, local);
391        }
392        return local;
393    }
394
395    /** A routine's own catalog for its transient tables, under the shared catalog's rules and DDL gate. */
396    static ProcedureEnvironment newProcedureEnvironment(TSQLEnv env, Context context) {
397        // The routine-local catalog is keyed by THIS analysis's rules and probed by them. Built
398        // from the bare vendor it ran on the vendor default while the enclosing environment ran
399        // the analysis's profile — the two halves of one lookup asking different questions.
400        ProcedureEnvironment local = new ProcedureEnvironment(env.getDBVendor(), env.getIdentifierProfile());
401        local.setEnableGetMetadataFromDDL(env.isEnableGetMetadataFromDDL());
402        local.setDefaultCatalogName(context.catalog);
403        local.setDefaultSchemaName(context.schema);
404        return local;
405    }
406
407    /**
408     * The routine whose body holds a statement: the nearest enclosing named routine. An anonymous
409     * block ({@code BEGIN ... END} parsed as a block statement) runs as part of the body around it,
410     * or of the top-level script.
411     */
412    static TStoredProcedureSqlStatement procedureOwner(TCustomSqlStatement statement) {
413        for (TCustomSqlStatement current = statement; current != null; current = current.getParentStmt()) {
414            if (current instanceof TStoredProcedureSqlStatement
415                    && ((TStoredProcedureSqlStatement) current).getStoredProcedureName() != null) {
416                return (TStoredProcedureSqlStatement) current;
417            }
418        }
419        return null;
420    }
421
422    private void record(TemporaryEvent event) {
423        if (event.body == null) {
424            scriptEvents.add(event);
425            return;
426        }
427        List<TemporaryEvent> events = routineEvents.get(event.body);
428        if (events == null) routineEvents.put(event.body, events = new ArrayList<>());
429        events.add(event);
430    }
431
432    private List<TemporaryEvent> eventsOf(TStoredProcedureSqlStatement body) {
433        if (body == null) return scriptEvents;
434        List<TemporaryEvent> events = routineEvents.get(body);
435        return events == null ? Collections.<TemporaryEvent>emptyList() : events;
436    }
437
438    /**
439     * A temporary table's key. A name that is temporary by its spelling ({@code #t}) is the same
440     * table whatever qualifies it; any other name is keyed with the defaults at its statement.
441     */
442    private static List<CanonKey> temporaryKey(TObjectName name, EDbVendor vendor, Context context, TSQLEnv env) {
443        IdentifierService service = env.getIdentifierService();
444        CanonKey object = service.canonKey(name.getObjectString(), ESQLDataObjectType.dotTable);
445        if (TemporaryTableSemantics.ofName(vendor, name) != null) return Collections.singletonList(object);
446        String catalog = SQLUtil.isEmpty(name.getDatabaseString()) ? context.catalog : name.getDatabaseString();
447        String schema = SQLUtil.isEmpty(name.getSchemaString()) ? context.schema : name.getSchemaString();
448        return Arrays.asList(service.canonKey(catalog, ESQLDataObjectType.dotCatalog),
449                service.canonKey(schema, ESQLDataObjectType.dotSchema), object);
450    }
451
452    /**
453     * Bind every reference to the transient table visible at it, whatever order the routines
454     * were declared in. Within a body the latest definition, or call that returns a session
455     * table, wins; a routine body that has none of its own sees what every one of its callers
456     * makes visible at the call; the top-level script's tables stay visible to the routines
457     * declared after them.
458     */
459    private void bindTemporaryTables(TSQLEnv env) {
460        if (transientTables.isEmpty()) return;
461        Set<List<CanonKey>> definedKeys = new HashSet<>();
462        List<List<TemporaryEvent>> bodies = new ArrayList<>(routineEvents.values());
463        bodies.add(scriptEvents);
464        for (List<TemporaryEvent> events : bodies) {
465            for (TemporaryEvent event : events) {
466                if (event.kind == TemporaryEvent.Kind.DEFINE) definedKeys.add(event.key);
467            }
468        }
469        Map<List<CanonKey>, List<TStoredProcedureSqlStatement>> declarations = new HashMap<>();
470        for (TCustomSqlStatement statement : contexts.keySet()) {
471            if (!(statement instanceof TStoredProcedureSqlStatement)) continue;
472            TStoredProcedureSqlStatement procedure = (TStoredProcedureSqlStatement) statement;
473            if (procedure.getStoredProcedureName() == null) continue;
474            List<CanonKey> key = procedureKey(procedure.getStoredProcedureName(), contexts.get(statement), env);
475            if (!declarations.containsKey(key)) declarations.put(key, new ArrayList<TStoredProcedureSqlStatement>());
476            declarations.get(key).add(procedure);
477        }
478        TemporaryScopes scopes = new TemporaryScopes();
479        for (List<TemporaryEvent> events : bodies) {
480            for (TemporaryEvent call : events) {
481                if (call.kind != TemporaryEvent.Kind.CALL) continue;
482                List<TStoredProcedureSqlStatement> targets = declarations.get(call.key);
483                if (targets == null || targets.size() != 1) continue;
484                call.target = targets.get(0);
485                if (!scopes.callers.containsKey(call.target)) {
486                    scopes.callers.put(call.target, new ArrayList<TemporaryEvent>());
487                }
488                scopes.callers.get(call.target).add(call);
489            }
490        }
491        for (TemporaryEvent event : scriptEvents) {
492            if (event.kind != TemporaryEvent.Kind.REFERENCE) scopes.script.add(event);
493        }
494        List<TemporaryEvent> unbound = new ArrayList<>();
495        for (List<TemporaryEvent> events : bodies) {
496            Map<List<CanonKey>, Object> visible = new HashMap<>();
497            for (TemporaryEvent event : events) {
498                switch (event.kind) {
499                    case DEFINE:
500                        visible.put(event.key, event.table);
501                        break;
502                    case DROP:
503                        // A DROP may sit in a branch that did not run: the table is still maybe
504                        // present, and a later reference is still to it, never to a catalog table.
505                        break;
506                    case CALL:
507                        event.visibleAtCall = new HashMap<>(visible);
508                        if (event.target != null) visible.putAll(scopes.exportsOf(event.target));
509                        break;
510                    case REFERENCE:
511                        if (!definedKeys.contains(event.key)) break;
512                        Object bound = visible.get(event.key);
513                        if (bound == null && event.body != null) unbound.add(event);
514                        else if (bound != null) apply(event, bound, env);
515                        break;
516                    default:
517                        break;
518                }
519            }
520        }
521        for (TemporaryEvent reference : unbound) {
522            Object bound = scopes.incoming(reference.body, reference.key);
523            if (bound == null) bound = scopes.scriptTableAt(reference.sequence, reference.key);
524            if (bound != null) apply(reference, bound, env);
525        }
526    }
527
528    /** The call graph and the tables it carries between routine bodies. */
529    private final class TemporaryScopes {
530        final Map<TStoredProcedureSqlStatement, List<TemporaryEvent>> callers = new IdentityHashMap<>();
531        /** The top-level script's definitions, drops and calls, in statement order. */
532        final List<TemporaryEvent> script = new ArrayList<>();
533        private final Map<TStoredProcedureSqlStatement, Map<List<CanonKey>, Object>> exports = new IdentityHashMap<>();
534        private final Set<TStoredProcedureSqlStatement> exporting = Collections.newSetFromMap(
535                new IdentityHashMap<TStoredProcedureSqlStatement, Boolean>());
536
537        /**
538         * The session tables a routine leaves to its caller when it returns. A table that lives
539         * only while the routine runs, or that the routine creates and drops, is not among them; a
540         * DROP of a table the caller had may not have run, so the caller keeps it.
541         */
542        Map<List<CanonKey>, Object> exportsOf(TStoredProcedureSqlStatement routine) {
543            Map<List<CanonKey>, Object> result = exports.get(routine);
544            if (result != null) return result;
545            // A recursive call returns nothing new while the routine's own effects are being computed.
546            if (!exporting.add(routine)) return Collections.emptyMap();
547            result = new HashMap<>();
548            Set<List<CanonKey>> created = new HashSet<>();
549            for (TemporaryEvent event : eventsOf(routine)) {
550                if (event.kind == TemporaryEvent.Kind.DEFINE) {
551                    created.add(event.key);
552                    if (event.scope == TemporaryTableSemantics.Scope.SESSION) result.put(event.key, event.table);
553                    else result.remove(event.key);
554                } else if (event.kind == TemporaryEvent.Kind.DROP) {
555                    if (created.contains(event.key)) result.remove(event.key);
556                } else if (event.kind == TemporaryEvent.Kind.CALL && event.target != null) {
557                    for (Map.Entry<List<CanonKey>, Object> returned : exportsOf(event.target).entrySet()) {
558                        created.add(returned.getKey());
559                        result.put(returned.getKey(), returned.getValue());
560                    }
561                }
562            }
563            exporting.remove(routine);
564            exports.put(routine, result);
565            return result;
566        }
567
568        /**
569         * What every caller of a routine makes visible under a key at its call: one table when
570         * they agree, {@link #CONFLICT} when they differ, null when some caller has none.
571         */
572        Object incoming(TStoredProcedureSqlStatement procedure, List<CanonKey> key) {
573            Set<TStoredProcedureSqlStatement> seen = Collections.newSetFromMap(
574                    new IdentityHashMap<TStoredProcedureSqlStatement, Boolean>());
575            Deque<TStoredProcedureSqlStatement> pending = new ArrayDeque<>();
576            pending.push(procedure);
577            Object result = null;
578            while (!pending.isEmpty()) {
579                TStoredProcedureSqlStatement current = pending.pop();
580                if (!seen.add(current)) continue;
581                List<TemporaryEvent> sites = callers.get(current);
582                if (sites == null || sites.isEmpty()) return null;
583                for (TemporaryEvent call : sites) {
584                    Object candidate = call.visibleAtCall == null ? null : call.visibleAtCall.get(key);
585                    if (candidate == null && call.body != null) candidate = scriptTableAt(call.sequence, key);
586                    if (candidate == null && call.body != null) {
587                        // Nothing visible in the caller: the caller's callers decide.
588                        pending.push(call.body);
589                    } else if (candidate == null) {
590                        return null;
591                    } else if (result == null) {
592                        result = candidate;
593                    } else if (result != candidate) {
594                        // Different callers supply different tables; do not invent one binding.
595                        return CONFLICT;
596                    }
597                }
598            }
599            return result;
600        }
601
602        /** The top-level script's table under a key just before a position: routines see the script before them. */
603        Object scriptTableAt(int sequence, List<CanonKey> key) {
604            Object result = null;
605            for (TemporaryEvent event : script) {
606                if (event.sequence >= sequence) break;
607                if (event.kind == TemporaryEvent.Kind.CALL) {
608                    if (event.target == null) continue;
609                    Object returned = exportsOf(event.target).get(key);
610                    if (returned != null) result = returned;
611                } else if (event.kind == TemporaryEvent.Kind.DEFINE && key.equals(event.key)) {
612                    result = event.table;
613                }
614            }
615            return result;
616        }
617    }
618
619    /**
620     * Bind a reference to the transient table visible at it, or, when callers disagree on which
621     * one, to none: some temporary table is visible there, so a catalog table of the name is not.
622     */
623    private void apply(TemporaryEvent reference, Object bound, TSQLEnv env) {
624        TTable table = reference.reference;
625        TSQLTable metadata = bound instanceof TSQLTable ? (TSQLTable) bound : null;
626        if (metadata != null) {
627            table.setSqlEnv(metadata.getSqlEnv());
628            table.setCatalogStatus(ETableCatalogStatus.FOUND);
629        } else {
630            if (reference.body != null) {
631                table.setSqlEnv(procedureEnvironment(reference.body, tableContexts.get(table), env));
632            }
633            table.setCatalogStatus(ETableCatalogStatus.NOT_CHECKED);
634        }
635        tables.put(table, metadata);
636        table.setResolvedTable(metadata);
637    }
638
639    private List<CanonKey> procedureKey(TObjectName name, Context context, TSQLEnv env) {
640        String catalog = SQLUtil.isEmpty(name.getDatabaseString()) ? context.catalog : name.getDatabaseString();
641        String schema = SQLUtil.isEmpty(name.getSchemaString()) ? context.schema : name.getSchemaString();
642        IdentifierService service = env.getIdentifierService();
643        return Arrays.asList(service.canonKey(catalog, ESQLDataObjectType.dotCatalog),
644                service.canonKey(schema, ESQLDataObjectType.dotSchema),
645                service.canonKey(name.getObjectString(), ESQLDataObjectType.dotProcedure));
646    }
647
648    /**
649     * The procedure a statement calls: {@code EXEC[UTE] <procedure>} in the SQL Server and ASE
650     * family, or {@code CALL <procedure>}. Null for anything else, including {@code EXEC} of a
651     * string.
652     */
653    private static TObjectName calledRoutine(TCustomSqlStatement statement) {
654        if (statement instanceof TMssqlExecute) {
655            TMssqlExecute exec = (TMssqlExecute) statement;
656            return exec.getExecType() == TBaseType.metExecSp ? exec.getModuleName() : null;
657        }
658        if (statement instanceof TCallStatement) {
659            return ((TCallStatement) statement).getRoutineName();
660        }
661        return null;
662    }
663
664    private static void pushStatements(Deque<TCustomSqlStatement> pending, TStatementList statements) {
665        if (statements == null) return;
666        for (int i = statements.size() - 1; i >= 0; i--) {
667            if (statements.get(i) != null) pending.push(statements.get(i));
668        }
669    }
670}