001package gudusoft.gsqlparser.dlineage.dynamicsql;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TCustomSqlStatement;
005import gudusoft.gsqlparser.TStatementList;
006
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011
012/**
013 * Analysis-scoped index of routine DEFINITIONS and top-level candidate CALL-SITE
014 * statements across the units of a multi-file analysis (plan §5.6, Phase 5
015 * slice 3 — the seed of the shared routine/signature index).
016 *
017 * <p>Lazily built and memoized per unit text: a unit is parsed (through the
018 * shared, caller-owned parse cache) and its statement list walked at most ONCE
019 * per analysis, no matter how many procedures consult the catalog. Callers keep
020 * their own cheap contains-prefilter, so units that never mention a routine of
021 * interest are still never parsed at all — cost semantics are identical to the
022 * previous per-procedure scans, minus the repeated walking.
023 *
024 * <p>The catalog only INDEXES; every matching, vetting, and ambiguity decision
025 * stays in {@link DynamicSqlLineageResolver} so this refactor is behavior-
026 * preserving by construction.
027 *
028 * <h3>How this differs from {@code TSQLEnv} / {@code TSQLProcedure}</h3>
029 *
030 * The sqlenv package already keeps a procedure registry
031 * ({@code gudusoft.gsqlparser.sqlenv.TSQLProcedure} under {@code TSQLSchema},
032 * with typed parameters and even the definition source string). The two serve
033 * different layers and are NOT redundant:
034 *
035 * <ul>
036 * <li><b>AST working set vs metadata registry.</b> {@code TSQLEnv} stores
037 *     metadata — names, parameter TYPES, the definition as a STRING — and
038 *     serves NAME RESOLUTION (resolver2/dlineage resolving {@code a.b.c}
039 *     against catalog objects). This catalog stores live
040 *     {@link gudusoft.gsqlparser.TCustomSqlStatement AST nodes} plus each
041 *     statement's unit provenance and effective USE database/schema, because
042 *     the dynamic-SQL binding machinery must hand the callee's AST to the
043 *     evaluator/interpreter and extract literal arguments from caller ASTs.
044 *     An env definition string would have to be re-parsed anyway — which is
045 *     exactly what this catalog's parse cache does, while also indexing what
046 *     the env has no concept of at all: <b>call sites</b>.</li>
047 * <li><b>Duplicate visibility vs registry merging.</b> An env is keyed by
048 *     qualified name, so re-registering a same-named routine merges or
049 *     overwrites. The binding rules ("never guess") need the opposite: every
050 *     definition OCCURRENCE with its origin unit must stay visible so a
051 *     same-named duplicate can be detected and refused. The information a
052 *     registry naturally collapses is precisely what the ambiguity verdicts
053 *     here require.</li>
054 * <li><b>No environment assumed.</b> dlineage frequently runs with no
055 *     {@code TSQLEnv} attached (and env construction is option-gated); this
056 *     catalog depends only on the analysis unit texts themselves.</li>
057 * <li><b>Planned convergence.</b> Plan §5.6 promotes one routine/signature
058 *     index shared by production dlineage and Analyzer v2. This class starts
059 *     analysis-side because behavior preservation was the gate; unifying it
060 *     with env registration and v2's {@code BoundProgram} is later Phase 5/6
061 *     work.</li>
062 * </ul>
063 *
064 * <h3>When metadata already contains stored-procedure definitions</h3>
065 *
066 * <ul>
067 * <li><b>Metadata carrying routine SOURCE</b> (sqlflow JSON {@code sourceCode},
068 *     grabit exports, DDL dumps) already flows in with no extra wiring: those
069 *     definitions become ordinary analysis units, so this catalog indexes them
070 *     like any other file while {@code SQLEnvParser} independently registers
071 *     them for name resolution.</li>
072 * <li><b>Metadata carrying only SIGNATURES</b> (name + parameter types, no
073 *     body — the catalog-input-interface direction) cannot produce lineage
074 *     THROUGH a body, but enables correct IN/OUT binding at call sites,
075 *     overload disambiguation, and a "known external routine, effects unknown"
076 *     diagnostic instead of a bare unresolved.</li>
077 * <li><b>Future slice ("env-backed routine bodies"):</b> when an attached env
078 *     holds a {@code TSQLRoutine} definition string for a routine that is NOT
079 *     among the analysis units, this catalog could parse that definition as a
080 *     virtual unit — covering the "caller file plus metadata-only definition"
081 *     scenario.</li>
082 * </ul>
083 */
084public final class RoutineCatalog {
085
086    /**
087     * B4a: everything needed to re-wrap ONE package member back into a
088     * standalone, analyzable single-member package.
089     *
090     * <p>A member routine node carries its own token range, so its
091     * {@code toString()} yields just {@code PROCEDURE foo(...) IS ... END foo}
092     * — but that fragment is not a parseable top-level statement. Wrapping it
093     * in a synthetic package that keeps the declaration section (globals) and
094     * drops every sibling body is what makes per-member summarization possible:
095     * globals stay in scope (arch-F3 boundary endpoints), while an absent
096     * sibling makes an intra-package call surface as a raw call site, enforcing
097     * no-inline-callee by construction exactly as {@code definitionSlice} does
098     * for top-level routines.
099     */
100    static final class PackageContext {
101        /** Qualified package name for the wrapper header. */
102        final String qualifiedName;
103        /** Simple package name for the trailing {@code END <name>;}. */
104        final String endName;
105        /** Concatenated non-routine declare elements (variables, cursors, types). */
106        final String globalsText;
107
108        PackageContext(String qualifiedName, String endName, String globalsText) {
109            this.qualifiedName = qualifiedName;
110            this.endName = endName;
111            this.globalsText = globalsText;
112        }
113    }
114
115    /** One routine definition found at the top level of a unit. */
116    static final class Definition {
117        final TCustomSqlStatement stmt;
118        /** mssql: explicit three-part catalog or effective USE database; oracle: null. */
119        final String db;
120        /** oracle: schema segment of a qualified definition name; mssql: unused (null). */
121        final String schema;
122        final String simple;
123        // ---- B0 additive signature/provenance fields (design §2.1a). The
124        // legacy matching fields above keep their exact prior semantics; the
125        // fields below are consumed only by summary-layer code (B2+), never
126        // by the shipped resolver paths — behavior-preserving by construction.
127        /** Kind discriminator; top-level definitions today are PROCEDURE/FUNCTION. */
128        final RoutineKind kind;
129        /** Raw package simple name for package members; null otherwise. */
130        final String packageName;
131        /** True for a package SPEC declaration (signature only, no body). */
132        final boolean specOnly;
133        /** Occurrence provenance: index of the defining statement in its unit. */
134        final int statementIndex;
135        /** Canonical signature descriptor (never used for legacy matching). */
136        final RoutineIdentity identity;
137        /** B4a: re-wrap context for package members; null for top-level defs. */
138        final PackageContext pkgContext;
139
140        Definition(TCustomSqlStatement stmt, String db, String schema, String simple,
141                RoutineKind kind, String packageName, boolean specOnly,
142                int statementIndex, RoutineIdentity identity) {
143            this(stmt, db, schema, simple, kind, packageName, specOnly,
144                    statementIndex, identity, null);
145        }
146
147        Definition(TCustomSqlStatement stmt, String db, String schema, String simple,
148                RoutineKind kind, String packageName, boolean specOnly,
149                int statementIndex, RoutineIdentity identity,
150                PackageContext pkgContext) {
151            this.stmt = stmt;
152            this.db = db;
153            this.schema = schema;
154            this.simple = simple;
155            this.kind = kind;
156            this.packageName = packageName;
157            this.specOnly = specOnly;
158            this.statementIndex = statementIndex;
159            this.identity = identity;
160            this.pkgContext = pkgContext;
161        }
162    }
163
164    /** One top-level non-definition statement — a potential caller. */
165    static final class CallCandidate {
166        final TCustomSqlStatement stmt;
167        /** mssql: effective USE database at the statement; oracle: null. */
168        final String db;
169
170        CallCandidate(TCustomSqlStatement stmt, String db) {
171            this.stmt = stmt;
172            this.db = db;
173        }
174    }
175
176    /** Everything the catalog knows about one unit. */
177    static final class UnitIndex {
178        final List<Definition> definitions = new ArrayList<Definition>();
179        final List<CallCandidate> callCandidates = new ArrayList<CallCandidate>();
180        /**
181         * B0: Oracle package-member definitions (spec declarations and body
182         * definitions), kept SEPARATE from {@link #definitions} so every
183         * shipped resolver path sees exactly the pre-B0 view. The enclosing
184         * package statement still appears in {@link #callCandidates} exactly
185         * as before (its body can contain call sites). Summary-layer slices
186         * (B2+) are the only consumers.
187         */
188        final List<Definition> memberDefinitions = new ArrayList<Definition>();
189    }
190
191    private final EDbVendor vendor;
192    private final List<String> unitTexts;
193    private final Map<String, TStatementList> parseCache;
194    private final Map<String, UnitIndex> unitIndexes = new HashMap<String, UnitIndex>();
195
196    public RoutineCatalog(EDbVendor vendor, List<String> unitTexts, Map<String, TStatementList> parseCache) {
197        this.vendor = vendor;
198        this.unitTexts = unitTexts;
199        this.parseCache = parseCache;
200    }
201
202    /** The raw texts of every analysis unit, in analysis order (includes the caller's own). */
203    List<String> unitTexts() {
204        return unitTexts;
205    }
206
207    /**
208     * Index of one unit; parses on first use (cached), walks the statement list on
209     * first use (memoized). Null when the unit failed to parse — a failed unit's
210     * statement boundaries are not trustworthy and contribute nothing.
211     */
212    UnitIndex indexOf(String unitText) {
213        if (unitIndexes.containsKey(unitText)) {
214            return unitIndexes.get(unitText);
215        }
216        TStatementList stmts;
217        if (parseCache.containsKey(unitText)) {
218            stmts = parseCache.get(unitText);
219        } else {
220            stmts = parseUnit(unitText);
221            parseCache.put(unitText, stmts);
222        }
223        UnitIndex index = stmts == null ? null : buildIndex(stmts);
224        unitIndexes.put(unitText, index);
225        return index;
226    }
227
228    private UnitIndex buildIndex(TStatementList stmts) {
229        UnitIndex index = new UnitIndex();
230        String currentDb = null;
231        for (int i = 0; i < stmts.size(); i++) {
232            TCustomSqlStatement stmt = stmts.get(i);
233            if (stmt instanceof gudusoft.gsqlparser.stmt.TUseDatabase) {
234                currentDb = String.valueOf(
235                        ((gudusoft.gsqlparser.stmt.TUseDatabase) stmt).getDatabaseName());
236                continue;
237            }
238            Definition definition = definitionOf(stmt, currentDb, i);
239            if (definition != null) {
240                index.definitions.add(definition);
241            } else {
242                indexPackageMembers(stmt, i, index);
243                index.callCandidates.add(new CallCandidate(stmt,
244                        vendor == EDbVendor.dbvmssql ? currentDb : null));
245            }
246        }
247        return index;
248    }
249
250    /** Vendor-specific definition extraction; null when {@code stmt} defines no routine. */
251    private Definition definitionOf(TCustomSqlStatement stmt, String currentDb,
252            int statementIndex) {
253        if (vendor == EDbVendor.dbvmssql) {
254            if (stmt instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure) {
255                gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure proc =
256                        (gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure) stmt;
257                if (proc.getProcedureName() == null) {
258                    return null;
259                }
260                String db = DynamicSqlLineageResolver.mssqlDefinitionDatabase(proc, currentDb);
261                // B3 (codex-B3-r1 finding 6): deprecated numbered procedures
262                // ("p;2") carry the ;N as the ratified overload discriminator
263                // — without it dbo.p;1 and dbo.p;2 collapse into duplicates.
264                // The parser DROPS ";N" from the AST name, so it is recovered
265                // from the token stream right after the name; the text-based
266                // helpers below stay as fallback for callers that carry it.
267                String objectText = proc.getProcedureName().getObjectString();
268                String discriminator = numberedGroupAfter(proc.getProcedureName());
269                if (discriminator == null) {
270                    discriminator = mssqlNumberedSuffixOf(objectText);
271                }
272                return new Definition(stmt, db, null,
273                        DynamicSqlLineageResolver.simpleNameOf(proc.getProcedureName().toString()),
274                        RoutineKind.PROCEDURE, null, false, statementIndex,
275                        identityOf(RoutineKind.PROCEDURE, db,
276                                proc.getProcedureName().getSchemaString(), null,
277                                stripMssqlNumberedSuffix(objectText), stmt,
278                                discriminator));
279            }
280            return null;
281        }
282        String simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf(stmt);
283        if (simple == null) {
284            return null;
285        }
286        RoutineKind kind =
287                stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction
288                        ? RoutineKind.FUNCTION : RoutineKind.PROCEDURE;
289        String schema = DynamicSqlLineageResolver.plsqlRoutineSchemaOf(stmt);
290        return new Definition(stmt, null, schema, simple, kind, null, false,
291                statementIndex, identityOf(kind, null, schema, null, simple, stmt));
292    }
293
294    /**
295     * B0: index Oracle package members (spec declarations and body
296     * definitions) into {@link UnitIndex#memberDefinitions}. The package
297     * statement itself is not consumed — it stays a call candidate exactly as
298     * before, so shipped matching behavior is untouched.
299     */
300    private void indexPackageMembers(TCustomSqlStatement stmt, int statementIndex,
301            UnitIndex index) {
302        if (!(stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage)) {
303            return;
304        }
305        gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage pkg =
306                (gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) stmt;
307        if (pkg.getPackageName() == null) {
308            return;
309        }
310        String pkgSimple = pkg.getPackageName().getObjectString();
311        String pkgSchema = pkg.getPackageName().getSchemaString();
312        boolean specOnly =
313                pkg.getKind() != gudusoft.gsqlparser.TBaseType.kind_create_body;
314        if (pkg.getDeclareStatements() == null) {
315            return;
316        }
317        // B4a: the declaration section is not exposed as a unit — derive it as
318        // the complement of the routine filter below (variables, cursors, types,
319        // pragmas). These are the package globals (arch-F3); they must travel
320        // with every member slice or a member referencing one loses that
321        // endpoint entirely.
322        StringBuilder globals = new StringBuilder();
323        for (int i = 0; i < pkg.getDeclareStatements().size(); i++) {
324            Object decl = pkg.getDeclareStatements().get(i);
325            if (decl instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction
326                    || decl instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) {
327                continue;
328            }
329            String declText = String.valueOf(decl);
330            if (declText == null || declText.length() == 0
331                    // non-identifier-compare: TParseTreeNode.toString() returns
332                    // null (rendered "null" by valueOf) when token bounds are
333                    // missing; such a declaration cannot be reproduced.
334                    || "null".equals(declText)) {
335                continue;
336            }
337            declText = declText.trim();
338            globals.append("  ").append(declText);
339            if (!declText.endsWith(";")) {
340                globals.append(";");
341            }
342            globals.append("\n");
343        }
344        PackageContext pkgContext = new PackageContext(
345                pkg.getPackageName().toString(), pkgSimple, globals.toString());
346        for (int i = 0; i < pkg.getDeclareStatements().size(); i++) {
347            Object member = pkg.getDeclareStatements().get(i);
348            RoutineKind kind;
349            String simple;
350            if (member instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) {
351                kind = RoutineKind.PACKAGE_MEMBER_FUNCTION;
352                simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf(
353                        (TCustomSqlStatement) member);
354            } else if (member instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) {
355                kind = RoutineKind.PACKAGE_MEMBER_PROCEDURE;
356                simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf(
357                        (TCustomSqlStatement) member);
358            } else {
359                continue;
360            }
361            if (simple == null) {
362                continue;
363            }
364            index.memberDefinitions.add(new Definition((TCustomSqlStatement) member,
365                    null, pkgSchema, simple, kind, pkgSimple, specOnly, statementIndex,
366                    identityOf(kind, null, pkgSchema, pkgSimple, simple,
367                            (TCustomSqlStatement) member),
368                    pkgContext));
369        }
370    }
371
372    /** Identity of one definition; parameter shape read from the routine AST. */
373    private RoutineIdentity identityOf(RoutineKind kind, String catalog, String schema,
374            String packageName, String simple, TCustomSqlStatement stmt) {
375        return identityOf(kind, catalog, schema, packageName, simple, stmt, null);
376    }
377
378    private RoutineIdentity identityOf(RoutineKind kind, String catalog, String schema,
379            String packageName, String simple, TCustomSqlStatement stmt,
380            String overloadDiscriminator) {
381        gudusoft.gsqlparser.nodes.TParameterDeclarationList params =
382                stmt instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement
383                        ? ((gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) stmt)
384                                .getParameterDeclarations()
385                        : null;
386        return RoutineIdentity.of(vendor, kind, catalog, schema, packageName, simple,
387                params, overloadDiscriminator);
388    }
389
390    /**
391     * The {@code ;N} group of a deprecated numbered procedure, scanned from
392     * the source tokens right after the name — the parser drops it from the
393     * AST name entirely. {@code ;1} normalizes to null ({@code p} and
394     * {@code p;1} are one identity in T-SQL).
395     */
396    public static String numberedGroupAfter(gudusoft.gsqlparser.nodes.TObjectName name) {
397        if (name == null || name.getEndToken() == null) {
398            return null;
399        }
400        gudusoft.gsqlparser.TSourceToken semi = name.getEndToken().nextSolidToken();
401        if (semi == null || !";".equals(semi.toString())) { // non-identifier-compare: punctuation token
402            return null;
403        }
404        gudusoft.gsqlparser.TSourceToken num = semi.nextSolidToken();
405        if (num == null) {
406            return null;
407        }
408        return canonicalGroup(num.toString().trim());
409    }
410
411    /** Numeric canonical form of a group suffix: {@code ";01"} ≡ {@code ";1"}
412     *  (codex-B3-r3 f6); group 1 ≡ the unnumbered procedure ⇒ null. */
413    private static String canonicalGroup(String text) {
414        if (text == null || text.isEmpty()) {
415            return null;
416        }
417        for (int i = 0; i < text.length(); i++) {
418            if (!Character.isDigit(text.charAt(i))) {
419                return null;
420            }
421        }
422        long value;
423        try {
424            value = Long.parseLong(text);
425        } catch (NumberFormatException overflow) {
426            return text; // absurdly long digit run: keep spelling, never crash
427        }
428        return value == 1 ? null : String.valueOf(value);
429    }
430
431    /** Trailing {@code ;N} of a deprecated numbered-procedure name, or null.
432     *  {@code ;1} is the unnumbered procedure itself in T-SQL, so it
433     *  normalizes to null — {@code p} and {@code p;1} are one identity. */
434    static String mssqlNumberedSuffixOf(String objectText) {
435        if (objectText == null) {
436            return null;
437        }
438        int semi = objectText.lastIndexOf(';');
439        if (semi < 0 || semi == objectText.length() - 1) {
440            return null;
441        }
442        return canonicalGroup(objectText.substring(semi + 1).trim());
443    }
444
445    /** The name with any trailing numeric {@code ;N} removed (any spelling —
446     *  {@code ;1}, {@code ;01}, {@code ;2} — the group itself is carried by
447     *  {@link #mssqlNumberedSuffixOf}). */
448    static String stripMssqlNumberedSuffix(String objectText) {
449        if (objectText == null) {
450            return null;
451        }
452        int semi = objectText.lastIndexOf(';');
453        if (semi <= 0 || semi == objectText.length() - 1) {
454            return objectText;
455        }
456        String suffix = objectText.substring(semi + 1).trim();
457        for (int i = 0; i < suffix.length(); i++) {
458            if (!Character.isDigit(suffix.charAt(i))) {
459                return objectText;
460            }
461        }
462        return objectText.substring(0, semi);
463    }
464
465    /** Parse of a unit's text; null when parsing failed or crashed. */
466    private TStatementList parseUnit(String sqlText) {
467        try {
468            gudusoft.gsqlparser.TGSqlParser parser = new gudusoft.gsqlparser.TGSqlParser(vendor);
469            parser.sqltext = sqlText;
470            if (parser.parse() != 0) {
471                return null;
472            }
473            return parser.getSqlstatements();
474        } catch (RuntimeException ex) {
475            return null;
476        } catch (Error err) {
477            if (err instanceof ThreadDeath) {
478                throw err;
479            }
480            // StackOverflow on a pathological unit must not kill the analysis.
481            return null;
482        }
483    }
484}