001package gudusoft.gsqlparser.resolver2.metadata; 002 003import gudusoft.gsqlparser.EResolverType; 004import gudusoft.gsqlparser.TCustomSqlStatement; 005import gudusoft.gsqlparser.TGSqlParser; 006import gudusoft.gsqlparser.TStatementList; 007import gudusoft.gsqlparser.nodes.TObjectName; 008import gudusoft.gsqlparser.resolver.TMetadataCollector; 009import gudusoft.gsqlparser.resolver2.context.DatabaseContextTracker; 010import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 011import gudusoft.gsqlparser.sqlenv.TSQLEnv; 012import gudusoft.gsqlparser.sqlenv.TSQLSchema; 013import gudusoft.gsqlparser.sqlenv.TSQLTable; 014import gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics; 015import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement; 016import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 017import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement; 018 019import java.util.ArrayDeque; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Deque; 023import java.util.IdentityHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028/** 029 * The tables a script declares, as the parse of each later statement sees them (Mantis 4797). 030 * 031 * <p>Phase 1 links a statement's columns as soon as the statement is parsed, but RESOLVER and resolver2 put the 032 * script's {@code CREATE TABLE} statements into the parser's catalog only after every statement was parsed, and 033 * {@link EResolverType#NONE} never does. This catalog takes each declaration when it is parsed, so Phase 1 of the 034 * statements after it reads it in every resolver type. It places a declaration where resolver2 does 035 * ({@link StatementMetadata}): in the context of the {@code USE} statements before it, and a routine's transient 036 * table in that routine's own catalog (Mantis 4715). A {@code CREATE TABLE ... AS SELECT} has the columns of its 037 * query, and a name temporary by its spelling ({@code #t}) is the same table in every database and schema.</p> 038 * 039 * <p>Only Phase 1 reads it, before the parser's catalog, and the parser drops it when the statements are parsed: the 040 * later passes register the declarations at their positions themselves, so a {@code CREATE OR REPLACE} never takes the 041 * place of a catalog table ahead of them. With no later pass the parser publishes into its catalog what resolver2 042 * would ({@link #publish}). A declaration of a table the parser's catalog already holds where it would put it (caller 043 * metadata, a DDL preload) is not copied unless it replaces it: Phase 1 reads that table, as resolver2 keeps it.</p> 044 */ 045public final class ParseTimeCatalog { 046 private final TSQLEnv shared; 047 private final TStatementList statements; 048 /** The context after the top-level statements before {@link #parsed}. */ 049 private final DatabaseContextTracker tracker; 050 private final Set<TCustomSqlStatement> seen = 051 Collections.newSetFromMap(new IdentityHashMap<TCustomSqlStatement, Boolean>()); 052 private int parsed; 053 /** The walk of the top-level statement being parsed; null until a statement in it asks for its context. */ 054 private Cursor cursor; 055 /** The script's declarations outside a routine's transient tables; created on the first one. */ 056 private TSQLEnv declared; 057 private final Map<TStoredProcedureSqlStatement, TSQLEnv> routines = new IdentityHashMap<>(); 058 /** What {@link #publish} replays into the parser's catalog, in order. */ 059 private final List<TCreateTableSqlStatement> publishable = new ArrayList<>(); 060 private final Map<TCreateTableSqlStatement, StatementMetadata.Context> contexts = new IdentityHashMap<>(); 061 062 /** 063 * @param shared the parser's catalog 064 * @param statements the parser's statements, parsed in this order 065 * @param catalog the database the script starts in, before any of its {@code USE} statements 066 * @param schema the schema the script starts in 067 */ 068 public ParseTimeCatalog(TSQLEnv shared, TStatementList statements, String catalog, String schema) { 069 this.shared = shared; 070 this.statements = statements; 071 this.tracker = new DatabaseContextTracker(catalog, schema); 072 } 073 074 /** The catalog of the parse {@code statement} belongs to while it is running, or null. */ 075 private static ParseTimeCatalog of(TCustomSqlStatement statement) { 076 TGSqlParser parser = statement == null ? null : statement.getGsqlparser(); 077 return parser == null ? null : parser.getParseTimeCatalog(); 078 } 079 080 /** Called before the top-level statement at {@code index} is parsed: the statements before it set the context. */ 081 public void advanceTo(int index) { 082 for (; parsed < index && parsed < statements.size(); parsed++) { 083 cursor = null; 084 replay(statements.get(parsed)); 085 } 086 } 087 088 /** Feed a parsed top-level statement and the statements under it to the tracker, depth first in source order. */ 089 private void replay(TCustomSqlStatement root) { 090 Deque<TCustomSqlStatement> pending = new ArrayDeque<>(); 091 pending.push(root); 092 while (!pending.isEmpty()) { 093 TCustomSqlStatement current = pending.pop(); 094 if (!seen.add(current)) continue; 095 tracker.processStatement(current); 096 TStatementList children = current.getStatements(); 097 for (int i = children.size() - 1; i >= 0; i--) { 098 if (children.get(i) != null) pending.push(children.get(i)); 099 } 100 } 101 } 102 103 /** 104 * Phase 1 of a {@code CREATE TABLE}: register the declared table where the statements after it look it up. 105 */ 106 public static void declare(TCreateTableSqlStatement create) { 107 ParseTimeCatalog catalog = of(create); 108 if (catalog != null && create.getTableName() != null) catalog.add(create); 109 } 110 111 /** 112 * Whether {@code SELECT ... INTO target} creates a transient table of the routine it is in ({@code INTO #t}), which 113 * resolver2 keeps in the routine's own catalog: a named routine's, not an anonymous block's, which runs as part of 114 * the script around it. 115 */ 116 public static boolean isRoutineTransient(TSelectSqlStatement select, TObjectName target) { 117 return (target != null) && TemporaryTableSemantics.hasTransientTables(select.dbvendor) 118 && TemporaryTableSemantics.isTransient(TemporaryTableSemantics.ofSelectInto(select.dbvendor, target)) 119 && (StatementMetadata.procedureOwner(select) != null); 120 } 121 122 /** 123 * Phase 1 of {@code SELECT ... INTO} a routine's transient table ({@link #isRoutineTransient}): register it in the 124 * routine's own catalog, as resolver2 does. Any other target is the parser's catalog's, where the statement 125 * registers it. 126 */ 127 public static void declareSelectInto(TSelectSqlStatement select, TObjectName target) { 128 ParseTimeCatalog catalog = of(select); 129 if (catalog == null || !isRoutineTransient(select, target)) return; 130 StatementMetadata.Context context = catalog.contextAt(select); 131 TSQLEnv local = catalog.routineCatalog(StatementMetadata.procedureOwner(select), context); 132 StatementMetadata.declareQueryTable(local, target, select, context.catalog, context.schema, false); 133 } 134 135 /** 136 * The table Phase 1 of {@code statement} reads under {@code database.schema.table} (the qualifiers may be empty) 137 * before the parser's catalog: a transient table its routine declared before it, or a table the script declared 138 * before it. Null when there is none or no parse is running. 139 */ 140 public static TSQLTable searchTable(TCustomSqlStatement statement, String database, String schema, String table) { 141 ParseTimeCatalog catalog = of(statement); 142 return (catalog == null || table == null) ? null : catalog.search(statement, database, schema, table); 143 } 144 145 /** 146 * Put the declarations into the parser's catalog at their positions, as resolver2 would, for a parse with no 147 * later pass ({@link EResolverType#NONE}): a table declared with its columns, and a top-level {@code #t}. 148 * resolver2 reads a plain {@code CREATE TABLE ... AS SELECT} from the statement, never from its catalog. 149 */ 150 public void publish() { 151 if (!shared.isEnableGetMetadataFromDDL()) return; 152 for (TCreateTableSqlStatement create : publishable) { 153 StatementMetadata.Context context = contexts.get(create); 154 if (TMetadataCollector.registerDeclaredTable(create, shared, context.catalog, context.schema) != null) continue; 155 if (isTransient(create) && (TemporaryTableSemantics.ofName(create.dbvendor, create.getTableName()) != null)) { 156 StatementMetadata.declareQueryTable(shared, create.getTableName(), create.getSubQuery(), 157 context.catalog, context.schema, false); 158 } 159 } 160 } 161 162 private void add(TCreateTableSqlStatement create) { 163 StatementMetadata.Context context = contextAt(create); 164 boolean transientTable = isTransient(create); 165 TStoredProcedureSqlStatement routine = transientTable ? StatementMetadata.procedureOwner(create) : null; 166 if (routine != null) { 167 declareInto(routineCatalog(routine, context), create, context, true); 168 return; 169 } 170 // The recovery that parses a CREATE TABLE again declares it again. 171 if (contexts.put(create, context) == null) publishable.add(create); 172 if (!TMetadataCollector.replacesTable(create) 173 && TMetadataCollector.findDeclaredTable(create, shared, context.catalog, context.schema) != null) { 174 return; 175 } 176 declareInto(declaredCatalog(), create, context, transientTable); 177 } 178 179 /** 180 * Put a declaration into {@code target}, a catalog of this one: the columns it declares, else the columns of its 181 * query; a transient table with neither ({@code LIKE}, a copy) is a table of its own whose shape is not known. 182 */ 183 private static void declareInto(TSQLEnv target, TCreateTableSqlStatement create, StatementMetadata.Context context, 184 boolean transientTable) { 185 if (TMetadataCollector.addDeclaredTable(create, target, context.catalog, context.schema) != null) return; 186 boolean replace = TMetadataCollector.replacesTable(create); 187 if (create.getSubQuery() == null && !transientTable) return; 188 if (!replace && TMetadataCollector.findDeclaredTable(create, target, context.catalog, context.schema) != null) { 189 return; 190 } 191 StatementMetadata.declareQueryTable(target, create.getTableName(), create.getSubQuery(), context.catalog, 192 context.schema, replace); 193 } 194 195 private TSQLTable search(TCustomSqlStatement statement, String database, String schema, String table) { 196 if (declared == null && routines.isEmpty()) return null; 197 // A name temporary by its spelling is the same table whatever qualifies it, as resolver2 keys it. 198 boolean anywhere = TemporaryTableSemantics.ofNameSegment(statement.dbvendor, table) != null; 199 StatementMetadata.Context context = anywhere ? null : contextAt(statement); 200 String name = nullToEmpty(database) + "." + nullToEmpty(schema) + "." + table; 201 TSQLTable found = null; 202 if (!routines.isEmpty()) { 203 TStoredProcedureSqlStatement routine = StatementMetadata.procedureOwner(statement); 204 TSQLEnv local = routine == null ? null : routines.get(routine); 205 if (local != null) found = anywhere ? findAnywhere(local, table) : lookUp(local, context, name); 206 } 207 if (found == null && declared != null) { 208 found = anywhere ? findAnywhere(declared, table) : lookUp(declared, context, name); 209 } 210 return found; 211 } 212 213 /** A lookup in {@code env}, a catalog of this one, in the context of the statement that reads it. */ 214 private static TSQLTable lookUp(TSQLEnv env, StatementMetadata.Context context, String qualifiedName) { 215 env.setDefaultCatalogName(context.catalog); 216 env.setDefaultSchemaName(context.schema); 217 return env.searchTable(qualifiedName); 218 } 219 220 private static TSQLTable findAnywhere(TSQLEnv env, String table) { 221 for (TSQLCatalog catalog : env.getCatalogList()) { 222 for (TSQLSchema schema : catalog.getSchemaList()) { 223 TSQLTable found = schema.findTable(table); 224 if (found != null) return found; 225 } 226 } 227 return null; 228 } 229 230 private static String nullToEmpty(String s) { 231 return s == null ? "" : s; 232 } 233 234 private static boolean isTransient(TCreateTableSqlStatement create) { 235 return TemporaryTableSemantics.hasTransientTables(create.dbvendor) 236 && TemporaryTableSemantics.isTransient(TemporaryTableSemantics.ofDeclaration(create)); 237 } 238 239 private TSQLEnv declaredCatalog() { 240 if (declared == null) { 241 // Not a routine's catalog: a plain environment under the parser catalog's rules and DDL gate. 242 declared = new TSQLEnv(shared.getDBVendor(), shared.getIdentifierProfile()) { 243 @Override public void initSQLEnv() { } 244 }; 245 declared.setEnableGetMetadataFromDDL(shared.isEnableGetMetadataFromDDL()); 246 } 247 return declared; 248 } 249 250 private TSQLEnv routineCatalog(TStoredProcedureSqlStatement routine, StatementMetadata.Context context) { 251 TSQLEnv local = routines.get(routine); 252 if (local == null) { 253 local = StatementMetadata.newProcedureEnvironment(shared, context); 254 routines.put(routine, local); 255 } 256 local.setDefaultCatalogName(context.catalog); 257 local.setDefaultSchemaName(context.schema); 258 return local; 259 } 260 261 /** 262 * The context {@code statement} runs in: the top-level statements before it, and the statements of its own 263 * top-level statement before it, in the order {@link StatementMetadata#collect} walks them. 264 */ 265 private StatementMetadata.Context contextAt(TCustomSqlStatement statement) { 266 TCustomSqlStatement top = statement.getAncestorStmt(); 267 if (cursor == null || cursor.root != top) cursor = new Cursor(top); 268 return cursor.contextOf(statement); 269 } 270 271 /** A statement whose child statements the walk is in, with the index of the next one. */ 272 private static final class Frame { 273 final TCustomSqlStatement statement; 274 int next; 275 276 Frame(TCustomSqlStatement statement) { 277 this.statement = statement; 278 } 279 } 280 281 /** 282 * The walk of a top-level statement {@link StatementMetadata#collect} makes, depth first in source order, taken as 283 * far as its statements are parsed and resumed as more are. A statement keeps the context the walk reached it in, 284 * so the walk is made once however often its statements ask. 285 */ 286 private final class Cursor { 287 final TCustomSqlStatement root; 288 /** The context after the statements visited so far. */ 289 private final DatabaseContextTracker at = 290 new DatabaseContextTracker(tracker.getCurrentDatabase(), tracker.getCurrentSchemaComponent()); 291 /** The statements from the root down to the last one visited. */ 292 private final List<Frame> path = new ArrayList<>(); 293 private final Map<TCustomSqlStatement, StatementMetadata.Context> reached = new IdentityHashMap<>(); 294 private StatementMetadata.Context current; 295 296 Cursor(TCustomSqlStatement root) { 297 this.root = root; 298 visit(root); 299 } 300 301 StatementMetadata.Context contextOf(TCustomSqlStatement statement) { 302 StatementMetadata.Context context = reached.get(statement); 303 while (context == null && advance()) context = reached.get(statement); 304 // A statement the walk cannot reach yet runs after every statement parsed so far. 305 return context != null ? context : current; 306 } 307 308 /** Visit the next statement parsed so far; false when the walk has reached every one. */ 309 boolean advance() { 310 for (int depth = path.size() - 1; depth >= 0; depth--) { 311 Frame frame = path.get(depth); 312 TStatementList children = frame.statement.getStatements(); 313 while (frame.next < children.size()) { 314 TCustomSqlStatement child = children.get(frame.next++); 315 if (child == null || reached.containsKey(child)) continue; 316 // A later sibling is parsed after the statements under the one before it. 317 while (path.size() > depth + 1) path.remove(path.size() - 1); 318 visit(child); 319 return true; 320 } 321 } 322 return false; 323 } 324 325 private void visit(TCustomSqlStatement statement) { 326 // A statement an earlier top-level statement holds too was walked there. 327 if (!seen.contains(statement)) at.processStatement(statement); 328 current = StatementMetadata.contextOf(at, statement.dbvendor, shared); 329 reached.put(statement, current); 330 path.add(new Frame(statement)); 331 } 332 } 333}