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}