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}