001package gudusoft.gsqlparser.dlineage.routine; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TCustomSqlStatement; 005import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind; 006import gudusoft.gsqlparser.dlineage.dataflow.model.ModelBindingManager; 007import gudusoft.gsqlparser.dlineage.dataflow.model.OraclePackage; 008import gudusoft.gsqlparser.dlineage.dataflow.model.Procedure; 009import gudusoft.gsqlparser.nodes.TFunctionCall; 010import gudusoft.gsqlparser.nodes.TObjectName; 011import gudusoft.gsqlparser.sqlenv.IdentifierService; 012import gudusoft.gsqlparser.stmt.TCommonBlock; 013import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement; 014import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage; 015 016import java.io.FileOutputStream; 017import java.io.IOException; 018import java.io.OutputStreamWriter; 019import java.io.Writer; 020import java.nio.charset.StandardCharsets; 021import java.util.ArrayList; 022import java.util.List; 023 024/** 025 * Measurement only (D3b.0): runs {@link RoutineRegistry} beside the legacy name-keyed routine registry and 026 * records, per declaration and per lookup, what each found. Enabled by the system property 027 * {@value #PROPERTY} naming a file the records are appended to (tab separated); off by default, when the 028 * callers do not evaluate its arguments. Nothing here throws into the analysis, and the typed registry is fed 029 * from the statements themselves, never from the models the legacy registry chose. 030 */ 031public final class RoutineShadow { 032 033 public static final String PROPERTY = "gsp.dlineage.routineShadow"; 034 035 private static final Object WRITE_LOCK = new Object(); 036 037 /** The shadow's per-analysis state, kept by the analysis's ModelBindingManager and cleared with it. */ 038 public static final class State { 039 final RoutineRegistry routines = new RoutineRegistry(); 040 final RoutineRegistry definitions = new RoutineRegistry(); 041 } 042 043 private RoutineShadow() { 044 } 045 046 public static boolean enabled() { 047 try { 048 return System.getProperty(PROPERTY) != null; 049 } catch (RuntimeException denied) { 050 return false; 051 } 052 } 053 054 private static State state(ModelBindingManager manager) { 055 return manager.routineShadowState(); 056 } 057 058 private static EDbVendor vendor() { 059 return ModelBindingManager.getGlobalVendor(); 060 } 061 062 private static IdentifierService service() { 063 return ModelBindingManager.getGlobalIdentifierService(); 064 } 065 066 private static TObjectName enclosingPackage() { 067 OraclePackage pkg = ModelBindingManager.getGlobalOraclePackage(); 068 if (pkg != null && pkg.getProcedureObject() instanceof TStoredProcedureSqlStatement) { 069 return ((TStoredProcedureSqlStatement) pkg.getProcedureObject()).getStoredProcedureName(); 070 } 071 return null; 072 } 073 074 static CatalogObjectKind kindOf(TCustomSqlStatement stmt) { 075 String type = stmt == null || stmt.sqlstatementtype == null ? "" : stmt.sqlstatementtype.name(); 076 type = type.toLowerCase(java.util.Locale.ROOT); 077 if (type.contains("trigger")) { 078 return CatalogObjectKind.TRIGGER; 079 } 080 if (type.contains("function")) { 081 return CatalogObjectKind.FUNCTION; 082 } 083 if (type.contains("procedure")) { 084 return CatalogObjectKind.PROCEDURE; 085 } 086 return CatalogObjectKind.ROUTINE; 087 } 088 089 /** The named routine a statement is inside of (block owners resolved), or null; a package ends the walk. */ 090 private static TCustomSqlStatement enclosingRoutine(TCustomSqlStatement stmt) { 091 TCustomSqlStatement current = stmt == null ? null : stmt.getParentStmt(); 092 while (current != null) { 093 if (current instanceof TCommonBlock && ((TCommonBlock) current).getBlockBody() != null 094 && ((TCommonBlock) current).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) { 095 current = (TStoredProcedureSqlStatement) ((TCommonBlock) current).getBlockBody().getParentObjectName(); 096 } 097 if (current instanceof TPlsqlCreatePackage) { 098 return null; 099 } 100 if (current instanceof TStoredProcedureSqlStatement 101 && ((TStoredProcedureSqlStatement) current).getStoredProcedureName() != null) { 102 return current; 103 } 104 current = current.getParentStmt(); 105 } 106 return null; 107 } 108 109 /** The routine a PL/SQL routine is local to (Oracle, Dameng), or null for a top-level or package routine. */ 110 private static Object localScopeOf(TStoredProcedureSqlStatement stmt) { 111 EDbVendor vendor = vendor(); 112 if (vendor != EDbVendor.dbvoracle && vendor != EDbVendor.dbvdameng) { 113 return null; 114 } 115 return enclosingRoutine(stmt); 116 } 117 118 /** The routines a statement is inside of, innermost first: the scopes whose local routines it sees. */ 119 private static List<Object> visibleScopes(TCustomSqlStatement at) { 120 List<Object> scopes = new ArrayList<Object>(); 121 if (at instanceof TStoredProcedureSqlStatement && ((TStoredProcedureSqlStatement) at).getStoredProcedureName() != null) { 122 scopes.add(at); 123 } 124 for (TCustomSqlStatement routine = enclosingRoutine(at); routine != null; routine = enclosingRoutine(routine)) { 125 scopes.add(routine); 126 } 127 return scopes; 128 } 129 130 /** 131 * A routine statement the legacy registry was asked to create a model for, and the model it returned 132 * (a new one, or an existing one it found by name). 133 */ 134 public static void declared(ModelBindingManager manager, String site, TStoredProcedureSqlStatement stmt, 135 Procedure legacy) { 136 try { 137 if (stmt == null || legacy == null || service() == null) { 138 return; 139 } 140 RoutineName name = RoutineName.declared(stmt.getStoredProcedureName(), kindOf(stmt), vendor(), service(), 141 enclosingPackage()); 142 if (name == null) { 143 write("DECL", site, String.valueOf(stmt.getStoredProcedureName()), "-", describe(legacy), 144 "UNREPRESENTABLE", "-", "-", "N"); 145 return; 146 } 147 RoutineDeclaration declaration = RoutineDeclaration.of(stmt, name, stmt.getParameterDeclarations(), 148 localScopeOf(stmt), vendor(), service()); 149 RoutineDeclaration existing = state(manager).routines.find(declaration.identity()); 150 Object legacyFirst = legacy.getProcedureObject(); 151 boolean legacyReused = legacyFirst != stmt; 152 boolean agree = legacyReused ? existing != null && existing.getRepresentative() == legacyFirst 153 : existing == null || existing.getRepresentative() == stmt; 154 state(manager).routines.declare(declaration); 155 write("DECL", site, name.toString(), String.valueOf(declaration.getTypes()), describe(legacy), 156 existing == null ? "NEW" : "SAME_IDENTITY", "-", 157 existing == null ? "-" : describe(existing.getRepresentative()), agree ? "Y" : "N"); 158 } catch (RuntimeException e) { 159 write("ERROR", site, String.valueOf(e), "-", "-", "-", "-", "-", "N"); 160 } 161 } 162 163 /** 164 * A routine the legacy registry was asked for by name from a call or an EXEC ({@code origin} is the call or 165 * name node): it returns an existing routine it found by name, or a new one made from {@code origin}. The 166 * typed registry looks the name up first and declares a routine only when nothing is found. 167 */ 168 public static void declaredFromCall(ModelBindingManager manager, String site, TObjectName written, Object origin, 169 Integer argumentCount, CatalogObjectKind wanted, Procedure legacy) { 170 try { 171 if (written == null || legacy == null || service() == null) { 172 return; 173 } 174 boolean legacyCreated = legacy.getProcedureObject() == origin; 175 List<RoutineName> names = RoutineName.interpretations(written, CatalogObjectKind.ROUTINE, vendor(), service(), 176 enclosingPackage()); 177 RoutineLookupResult result = names.isEmpty() ? RoutineLookupResult.NOT_FOUND 178 : state(manager).routines.lookup(names, argumentCount, wanted, null); 179 Object resolved = result.getResolved() == null ? null : result.getResolved().getRepresentative(); 180 boolean agree = legacyCreated ? result.getStatus() == RoutineLookupResult.Status.NOT_FOUND 181 : resolved == legacy.getProcedureObject(); 182 write("DECL_CALL", site, names.isEmpty() ? String.valueOf(written) : names.get(0).toString(), 183 String.valueOf(argumentCount), (legacyCreated ? "new:" : "found:") + describe(legacy), 184 result.getStatus().name(), result.getBranch().name(), candidates(result), agree ? "Y" : "N"); 185 if (legacyCreated && !names.isEmpty() && result.getStatus() == RoutineLookupResult.Status.NOT_FOUND) { 186 // A placeholder for a routine never declared here, of the kind the call site asks for (a DB2 CALL 187 // creates a procedure, not a routine every lookup matches). 188 RoutineName name = RoutineName.declared(written, wanted, vendor(), service(), null); 189 if (name != null) { 190 state(manager).routines.declare(RoutineDeclaration.ofCall(origin, name, argumentCount, vendor())); 191 } 192 } 193 } catch (RuntimeException e) { 194 write("ERROR", site, String.valueOf(e), "-", "-", "-", "-", "-", "N"); 195 } 196 } 197 198 /** A routine definition found by the script pre-scan, analyzed lazily when first called. */ 199 public static void definition(ModelBindingManager manager, TStoredProcedureSqlStatement stmt) { 200 try { 201 if (stmt == null || service() == null) { 202 return; 203 } 204 RoutineName name = RoutineName.declared(stmt.getStoredProcedureName(), kindOf(stmt), vendor(), service(), 205 null); 206 if (name != null) { 207 state(manager).definitions.declare(RoutineDeclaration.of(stmt, name, stmt.getParameterDeclarations(), 208 null, vendor(), service())); 209 } 210 } catch (RuntimeException e) { 211 write("ERROR", "definition", String.valueOf(e), "-", "-", "-", "-", "-", "N"); 212 } 213 } 214 215 /** 216 * The argument alias a call is looked up with: its argument count where the vendor overloads routines. A 217 * function call always has its parentheses, so one the parser gives no argument list has none (DB2 218 * {@code TABLE(f())}). 219 */ 220 public static Integer countAliasOf(TFunctionCall call) { 221 if (call == null || vendor() == null || !RoutineDeclaration.callsCounted(vendor())) { 222 return null; 223 } 224 return Integer.valueOf(call.getArgs() == null ? 0 : call.getArgs().size()); 225 } 226 227 /** 228 * A callee lookup at a call site inside {@code at} and the routine the legacy lookup settled on (null when 229 * none). {@code aliases} are the argument aliases the site tries in order (null for a name-only lookup), as the 230 * site's own retries do. The script's pre-scanned definitions are looked up too, as the lazy analysis would. 231 */ 232 public static void lookedUp(ModelBindingManager manager, String site, TCustomSqlStatement at, TObjectName written, 233 Integer[] aliases, CatalogObjectKind wanted, Procedure legacy) { 234 try { 235 if (written == null || service() == null) { 236 return; 237 } 238 List<RoutineName> names = RoutineName.interpretations(written, CatalogObjectKind.ROUTINE, vendor(), service(), 239 enclosingPackage()); 240 if (names.isEmpty()) { 241 write("LOOKUP", site, String.valueOf(written), "-", describe(legacy), "UNREPRESENTABLE", "-", "-", 242 legacy == null ? "Y" : "N"); 243 return; 244 } 245 List<Object> scopes = visibleScopes(at); 246 RoutineLookupResult result = RoutineLookupResult.NOT_FOUND; 247 boolean viaDefinitions = false; 248 Integer used = null; 249 for (Integer alias : aliases) { 250 used = alias; 251 result = state(manager).routines.lookup(names, alias, wanted, scopes); 252 if (result.getStatus() != RoutineLookupResult.Status.NOT_FOUND) { 253 break; 254 } 255 } 256 if (result.getStatus() == RoutineLookupResult.Status.NOT_FOUND) { 257 // As the lazy analysis does: a routine the script defines later is found among its definitions. 258 for (Integer alias : aliases) { 259 RoutineLookupResult definitions = state(manager).definitions.lookup(names, alias, wanted, scopes); 260 if (definitions.getStatus() != RoutineLookupResult.Status.NOT_FOUND) { 261 result = definitions; 262 used = alias; 263 viaDefinitions = true; 264 break; 265 } 266 } 267 } 268 result = result.withLegacyWinner(legacy); 269 Object resolved = result.getResolved() == null ? null : result.getResolved().getRepresentative(); 270 Object legacyFirst = legacy == null ? null : legacy.getProcedureObject(); 271 boolean agree = resolved == legacyFirst 272 && (legacy != null || result.getStatus() == RoutineLookupResult.Status.NOT_FOUND); 273 write("LOOKUP", site, names.get(0).toString(), String.valueOf(used), describe(legacy), 274 result.getStatus().name(), result.getBranch().name(), 275 candidates(result) + (viaDefinitions ? " via:ddl" : ""), agree ? "Y" : "N"); 276 } catch (RuntimeException e) { 277 write("ERROR", site, String.valueOf(e), "-", "-", "-", "-", "-", "N"); 278 } 279 } 280 281 private static String candidates(RoutineLookupResult result) { 282 StringBuilder out = new StringBuilder(); 283 for (RoutineDeclaration candidate : result.getCandidates()) { 284 out.append(out.length() == 0 ? "" : ",").append(describe(candidate.getRepresentative())); 285 } 286 return out.length() == 0 ? "-" : out.toString(); 287 } 288 289 private static String describe(Object model) { 290 if (model == null) { 291 return "-"; 292 } 293 if (model instanceof Procedure) { 294 Procedure p = (Procedure) model; 295 return p.getName() + "#" + p.getId() + "<" + describe(p.getProcedureObject()) + ">"; 296 } 297 if (model instanceof TStoredProcedureSqlStatement) { 298 TStoredProcedureSqlStatement s = (TStoredProcedureSqlStatement) model; 299 return s.getStoredProcedureName() + "@" + (s.getStartToken() == null ? "?" : String.valueOf(s.getStartToken().lineNo)); 300 } 301 if (model instanceof TObjectName || model instanceof TFunctionCall) { 302 return model + "@call"; 303 } 304 return String.valueOf(model); 305 } 306 307 private static void write(String event, String site, String name, String alias, String legacy, String status, 308 String branch, String candidates, String agree) { 309 try { 310 String path = System.getProperty(PROPERTY); 311 if (path == null) { 312 return; 313 } 314 StringBuilder line = new StringBuilder(); 315 for (String field : new String[] {event, String.valueOf(vendor()), site, name, alias, legacy, status, branch, 316 candidates, agree}) { 317 if (line.length() > 0) { 318 line.append('\t'); 319 } 320 line.append(field == null ? "-" : field.replace('\t', ' ').replace('\n', ' ').replace('\r', ' ')); 321 } 322 line.append('\n'); 323 synchronized (WRITE_LOCK) { 324 try (Writer out = new OutputStreamWriter(new FileOutputStream(path, true), StandardCharsets.UTF_8)) { 325 out.write(line.toString()); 326 } 327 } 328 } catch (IOException | RuntimeException ignored) { 329 // Measurement output only; the analysis goes on without it. 330 } 331 } 332}