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 /** The parsed statements of one unit (parsed on first use, cached); null when it failed to parse. */ 208 TStatementList statementsOf(String unitText) { 209 indexOf(unitText); 210 return parseCache.get(unitText); 211 } 212 213 /** 214 * Index of one unit; parses on first use (cached), walks the statement list on 215 * first use (memoized). Null when the unit failed to parse — a failed unit's 216 * statement boundaries are not trustworthy and contribute nothing. 217 */ 218 UnitIndex indexOf(String unitText) { 219 if (unitIndexes.containsKey(unitText)) { 220 return unitIndexes.get(unitText); 221 } 222 TStatementList stmts; 223 if (parseCache.containsKey(unitText)) { 224 stmts = parseCache.get(unitText); 225 } else { 226 stmts = parseUnit(unitText); 227 parseCache.put(unitText, stmts); 228 } 229 UnitIndex index = stmts == null ? null : buildIndex(stmts); 230 unitIndexes.put(unitText, index); 231 return index; 232 } 233 234 private UnitIndex buildIndex(TStatementList stmts) { 235 UnitIndex index = new UnitIndex(); 236 String currentDb = null; 237 for (int i = 0; i < stmts.size(); i++) { 238 TCustomSqlStatement stmt = stmts.get(i); 239 if (stmt instanceof gudusoft.gsqlparser.stmt.TUseDatabase) { 240 currentDb = String.valueOf( 241 ((gudusoft.gsqlparser.stmt.TUseDatabase) stmt).getDatabaseName()); 242 continue; 243 } 244 Definition definition = definitionOf(stmt, currentDb, i); 245 if (definition != null) { 246 index.definitions.add(definition); 247 } else { 248 indexPackageMembers(stmt, i, index); 249 index.callCandidates.add(new CallCandidate(stmt, 250 vendor == EDbVendor.dbvmssql ? currentDb : null)); 251 } 252 } 253 return index; 254 } 255 256 /** Vendor-specific definition extraction; null when {@code stmt} defines no routine. */ 257 private Definition definitionOf(TCustomSqlStatement stmt, String currentDb, 258 int statementIndex) { 259 if (vendor == EDbVendor.dbvmssql) { 260 if (stmt instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure) { 261 gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure proc = 262 (gudusoft.gsqlparser.stmt.mssql.TMssqlCreateProcedure) stmt; 263 if (proc.getProcedureName() == null) { 264 return null; 265 } 266 String db = DynamicSqlLineageResolver.mssqlDefinitionDatabase(proc, currentDb); 267 // B3 (codex-B3-r1 finding 6): deprecated numbered procedures 268 // ("p;2") carry the ;N as the ratified overload discriminator 269 // — without it dbo.p;1 and dbo.p;2 collapse into duplicates. 270 // The parser DROPS ";N" from the AST name, so it is recovered 271 // from the token stream right after the name; the text-based 272 // helpers below stay as fallback for callers that carry it. 273 String objectText = proc.getProcedureName().getObjectString(); 274 String discriminator = numberedGroupAfter(proc.getProcedureName()); 275 if (discriminator == null) { 276 discriminator = mssqlNumberedSuffixOf(objectText); 277 } 278 return new Definition(stmt, db, null, 279 DynamicSqlLineageResolver.simpleNameOf(proc.getProcedureName().toString()), 280 RoutineKind.PROCEDURE, null, false, statementIndex, 281 identityOf(RoutineKind.PROCEDURE, db, 282 proc.getProcedureName().getSchemaString(), null, 283 stripMssqlNumberedSuffix(objectText), stmt, 284 discriminator)); 285 } 286 return null; 287 } 288 String simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf(stmt); 289 if (simple == null) { 290 return null; 291 } 292 RoutineKind kind = 293 stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction 294 ? RoutineKind.FUNCTION : RoutineKind.PROCEDURE; 295 String schema = DynamicSqlLineageResolver.plsqlRoutineSchemaOf(stmt); 296 return new Definition(stmt, null, schema, simple, kind, null, false, 297 statementIndex, identityOf(kind, null, schema, null, simple, stmt)); 298 } 299 300 /** 301 * B0: index Oracle package members (spec declarations and body 302 * definitions) into {@link UnitIndex#memberDefinitions}. The package 303 * statement itself is not consumed — it stays a call candidate exactly as 304 * before, so shipped matching behavior is untouched. 305 */ 306 private void indexPackageMembers(TCustomSqlStatement stmt, int statementIndex, 307 UnitIndex index) { 308 if (!(stmt instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage)) { 309 return; 310 } 311 gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage pkg = 312 (gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) stmt; 313 if (pkg.getPackageName() == null) { 314 return; 315 } 316 String pkgSimple = pkg.getPackageName().getObjectString(); 317 String pkgSchema = pkg.getPackageName().getSchemaString(); 318 boolean specOnly = 319 pkg.getKind() != gudusoft.gsqlparser.TBaseType.kind_create_body; 320 if (pkg.getDeclareStatements() == null) { 321 return; 322 } 323 // B4a: the declaration section is not exposed as a unit — derive it as 324 // the complement of the routine filter below (variables, cursors, types, 325 // pragmas). These are the package globals (arch-F3); they must travel 326 // with every member slice or a member referencing one loses that 327 // endpoint entirely. 328 StringBuilder globals = new StringBuilder(); 329 for (int i = 0; i < pkg.getDeclareStatements().size(); i++) { 330 Object decl = pkg.getDeclareStatements().get(i); 331 if (decl instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction 332 || decl instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) { 333 continue; 334 } 335 String declText = String.valueOf(decl); 336 if (declText == null || declText.length() == 0 337 // non-identifier-compare: TParseTreeNode.toString() returns 338 // null (rendered "null" by valueOf) when token bounds are 339 // missing; such a declaration cannot be reproduced. 340 || "null".equals(declText)) { 341 continue; 342 } 343 declText = declText.trim(); 344 globals.append(" ").append(declText); 345 if (!declText.endsWith(";")) { 346 globals.append(";"); 347 } 348 globals.append("\n"); 349 } 350 PackageContext pkgContext = new PackageContext( 351 pkg.getPackageName().toString(), pkgSimple, globals.toString()); 352 for (int i = 0; i < pkg.getDeclareStatements().size(); i++) { 353 Object member = pkg.getDeclareStatements().get(i); 354 RoutineKind kind; 355 String simple; 356 if (member instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction) { 357 kind = RoutineKind.PACKAGE_MEMBER_FUNCTION; 358 simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf( 359 (TCustomSqlStatement) member); 360 } else if (member instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure) { 361 kind = RoutineKind.PACKAGE_MEMBER_PROCEDURE; 362 simple = DynamicSqlLineageResolver.plsqlRoutineSimpleNameOf( 363 (TCustomSqlStatement) member); 364 } else { 365 continue; 366 } 367 if (simple == null) { 368 continue; 369 } 370 index.memberDefinitions.add(new Definition((TCustomSqlStatement) member, 371 null, pkgSchema, simple, kind, pkgSimple, specOnly, statementIndex, 372 identityOf(kind, null, pkgSchema, pkgSimple, simple, 373 (TCustomSqlStatement) member), 374 pkgContext)); 375 } 376 } 377 378 /** Identity of one definition; parameter shape read from the routine AST. */ 379 private RoutineIdentity identityOf(RoutineKind kind, String catalog, String schema, 380 String packageName, String simple, TCustomSqlStatement stmt) { 381 return identityOf(kind, catalog, schema, packageName, simple, stmt, null); 382 } 383 384 private RoutineIdentity identityOf(RoutineKind kind, String catalog, String schema, 385 String packageName, String simple, TCustomSqlStatement stmt, 386 String overloadDiscriminator) { 387 gudusoft.gsqlparser.nodes.TParameterDeclarationList params = 388 stmt instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement 389 ? ((gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) stmt) 390 .getParameterDeclarations() 391 : null; 392 return RoutineIdentity.of(vendor, kind, catalog, schema, packageName, simple, 393 params, overloadDiscriminator); 394 } 395 396 /** 397 * The {@code ;N} group of a deprecated numbered procedure, scanned from 398 * the source tokens right after the name — the parser drops it from the 399 * AST name entirely. {@code ;1} normalizes to null ({@code p} and 400 * {@code p;1} are one identity in T-SQL). 401 */ 402 public static String numberedGroupAfter(gudusoft.gsqlparser.nodes.TObjectName name) { 403 if (name == null || name.getEndToken() == null) { 404 return null; 405 } 406 gudusoft.gsqlparser.TSourceToken semi = name.getEndToken().nextSolidToken(); 407 if (semi == null || !";".equals(semi.toString())) { // non-identifier-compare: punctuation token 408 return null; 409 } 410 gudusoft.gsqlparser.TSourceToken num = semi.nextSolidToken(); 411 if (num == null) { 412 return null; 413 } 414 return canonicalGroup(num.toString().trim()); 415 } 416 417 /** Numeric canonical form of a group suffix: {@code ";01"} ≡ {@code ";1"} 418 * (codex-B3-r3 f6); group 1 ≡ the unnumbered procedure ⇒ null. */ 419 private static String canonicalGroup(String text) { 420 if (text == null || text.isEmpty()) { 421 return null; 422 } 423 for (int i = 0; i < text.length(); i++) { 424 if (!Character.isDigit(text.charAt(i))) { 425 return null; 426 } 427 } 428 long value; 429 try { 430 value = Long.parseLong(text); 431 } catch (NumberFormatException overflow) { 432 return text; // absurdly long digit run: keep spelling, never crash 433 } 434 return value == 1 ? null : String.valueOf(value); 435 } 436 437 /** Trailing {@code ;N} of a deprecated numbered-procedure name, or null. 438 * {@code ;1} is the unnumbered procedure itself in T-SQL, so it 439 * normalizes to null — {@code p} and {@code p;1} are one identity. */ 440 static String mssqlNumberedSuffixOf(String objectText) { 441 if (objectText == null) { 442 return null; 443 } 444 int semi = objectText.lastIndexOf(';'); 445 if (semi < 0 || semi == objectText.length() - 1) { 446 return null; 447 } 448 return canonicalGroup(objectText.substring(semi + 1).trim()); 449 } 450 451 /** The name with any trailing numeric {@code ;N} removed (any spelling — 452 * {@code ;1}, {@code ;01}, {@code ;2} — the group itself is carried by 453 * {@link #mssqlNumberedSuffixOf}). */ 454 static String stripMssqlNumberedSuffix(String objectText) { 455 if (objectText == null) { 456 return null; 457 } 458 int semi = objectText.lastIndexOf(';'); 459 if (semi <= 0 || semi == objectText.length() - 1) { 460 return objectText; 461 } 462 String suffix = objectText.substring(semi + 1).trim(); 463 for (int i = 0; i < suffix.length(); i++) { 464 if (!Character.isDigit(suffix.charAt(i))) { 465 return objectText; 466 } 467 } 468 return objectText.substring(0, semi); 469 } 470 471 /** Parse of a unit's text; null when parsing failed or crashed. */ 472 private TStatementList parseUnit(String sqlText) { 473 try { 474 gudusoft.gsqlparser.TGSqlParser parser = new gudusoft.gsqlparser.TGSqlParser(vendor); 475 parser.sqltext = sqlText; 476 if (parser.parse() != 0) { 477 return null; 478 } 479 return parser.getSqlstatements(); 480 } catch (RuntimeException ex) { 481 return null; 482 } catch (Error err) { 483 if (err instanceof ThreadDeath) { 484 throw err; 485 } 486 // StackOverflow on a pathological unit must not kill the analysis. 487 return null; 488 } 489 } 490}