001package gudusoft.gsqlparser.dlineage.routine; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TSourceToken; 005import gudusoft.gsqlparser.dlineage.dataflow.model.ModelBindingManager; 006import gudusoft.gsqlparser.nodes.TObjectName; 007import gudusoft.gsqlparser.sqlenv.CanonKey; 008import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 009import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 010import gudusoft.gsqlparser.sqlenv.IdentifierService; 011import gudusoft.gsqlparser.util.SQLUtil; 012 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.List; 016 017/** 018 * A key of the routine alias registry, the lineage model's name-keyed index of routines, on typed names. 019 * 020 * <p>The legacy index keys a routine by text: its name folded by the legacy normal-name function, followed by one of 021 * three alias suffixes rendered from the declaration ({@code (int,int)}, {@code (2)}, {@code (in1)}), appended 022 * unchanged. This key keeps that suffix as the same raw text, so aliases and overloads behave exactly as before, and 023 * replaces the folded name with the name's levels as written, in positional order, each keyed by the analysis's 024 * identifier rules. The routine level is keyed under the routine rules. Every qualifier is keyed under the schema 025 * rules, whether it is a server, a database, a schema or a package: which one it is cannot be known at a call, and 026 * two keys compare level by level.</p> 027 * 028 * <p>An explicitly empty middle level ({@code db..p}) is a level of its own, so {@code db..p} keeps three levels, as 029 * its legacy text did. A database link ({@code p@link}) is part of the key, as the legacy kept {@code @link} in the 030 * text.</p> 031 */ 032public final class RoutineAliasKey { 033 034 /** The routine level's comparison type. Procedures and functions share one index, and one rule group. */ 035 static final ESQLDataObjectType ROUTINE_TYPE = ESQLDataObjectType.dotProcedure; 036 /** Every qualifier's comparison type. */ 037 static final ESQLDataObjectType QUALIFIER_TYPE = ESQLDataObjectType.dotSchema; 038 039 /** The level of the empty qualifier in {@code db..p}. */ 040 private static final Object EMPTY_LEVEL = new Object() { 041 @Override 042 public String toString() { 043 return "<empty>"; 044 } 045 }; 046 047 private final List<Object> levels; 048 private final CanonKey dblink; 049 private final String signature; 050 private final CanonKey storedSignature; 051 /** 052 * Whether a level's text as written contains {@code (} ({@code [P(]}): the legacy lookup chose its branch by a 053 * {@code (} anywhere in the key text, so such a name is looked up as the legacy looked up a name with a suffix. 054 */ 055 private final boolean parenInName; 056 /** 057 * The routine level keyed as a qualifier: a package's name is a routine-level name of its own, and a qualifier of 058 * its members' names ({@link #qualifiedBy}), where it must compare with a call's qualifier. 059 */ 060 private final CanonKey routineAsQualifier; 061 private final int hash; 062 063 private RoutineAliasKey(List<Object> levels, CanonKey dblink, String signature, CanonKey storedSignature, 064 boolean parenInName, CanonKey routineAsQualifier) { 065 this.levels = Collections.unmodifiableList(levels); 066 this.dblink = dblink; 067 this.signature = signature; 068 this.storedSignature = storedSignature; 069 this.parenInName = parenInName; 070 this.routineAsQualifier = routineAsQualifier; 071 int h = levels.hashCode(); 072 h = 31 * h + (dblink == null ? 0 : dblink.hashCode()); 073 h = 31 * h + (signature == null ? 0 : signature.hashCode()); 074 this.hash = h; 075 } 076 077 /** The analysis's identifier service, or the vendor default when no analysis has published one. */ 078 public static IdentifierService service() { 079 IdentifierService service = ModelBindingManager.getGlobalIdentifierService(); 080 if (service != null) { 081 return service; 082 } 083 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 084 return vendor == null ? null : IdentifierService.defaultServiceFor(vendor); 085 } 086 087 /** 088 * The key of a routine name as written at a call or a declaration, with the legacy alias suffix 089 * {@code signature} (null when the legacy key had none). Null when the name has no routine level. 090 */ 091 public static RoutineAliasKey written(TObjectName name, String signature) { 092 return written(name, signature, service()); 093 } 094 095 public static RoutineAliasKey written(TObjectName name, String signature, IdentifierService service) { 096 if (name == null || service == null) { 097 return null; 098 } 099 try { 100 return writtenKey(name, signature, service); 101 } catch (RuntimeException unkeyable) { 102 // A name the identifier rules cannot key (it could not be declared or called either) names nothing here. 103 return null; 104 } 105 } 106 107 private static RoutineAliasKey writtenKey(TObjectName name, String signature, IdentifierService service) { 108 TSourceToken server = name.getServerToken(); 109 TSourceToken database = name.getDatabaseToken(); 110 TSourceToken schema = name.getSchemaToken(); 111 TSourceToken pkg = name.getPackageToken(); 112 TSourceToken object = name.getObjectToken(); 113 TSourceToken part = name.getPartToken(); 114 if (part != null && !part.toString().isEmpty()) { 115 // A name the parser split as object.part (DB2 CALL s.p): the part is the routine. 116 if (database == null) { 117 database = schema; 118 } 119 schema = object; 120 object = part; 121 } 122 if (object == null || object.toString().isEmpty()) { 123 return null; 124 } 125 List<Object> levels = new ArrayList<Object>(5); 126 boolean paren = false; 127 for (TSourceToken qualifier : new TSourceToken[] {server, database, schema, pkg}) { 128 if (qualifier != null) { 129 levels.add(qualifierLevel(qualifier.toString(), service)); 130 paren |= qualifier.toString().indexOf('(') >= 0; 131 } 132 } 133 levels.add(routineKey(object.toString(), service)); 134 paren |= object.toString().indexOf('(') >= 0; 135 CanonKey dblink = null; 136 if (name.getDblink() != null && name.getDblink().toString() != null && !name.getDblink().toString().isEmpty()) { 137 dblink = service.canonKeyOf(name.getDblink().toString(), ESQLDataObjectType.dotDblink, 138 IdentifierInputForm.SQL_LEXICAL); 139 paren |= name.getDblink().toString().indexOf('(') >= 0; 140 } 141 return new RoutineAliasKey(levels, dblink, signature, storedSignature(signature, service), paren, 142 qualifierKey(object.toString(), service)); 143 } 144 145 /** 146 * The key of a name whose levels are already separate identifiers as written: a model's database and schema 147 * fields and its routine name. Never a way to split a rendered, dotted name; an empty level is the empty 148 * qualifier of {@code db..p}. 149 */ 150 public static RoutineAliasKey ofWrittenLevels(List<String> qualifiers, String routine, String signature, 151 IdentifierService service) { 152 if (routine == null || routine.isEmpty() || service == null) { 153 return null; 154 } 155 List<Object> levels = new ArrayList<Object>(); 156 boolean paren = routine.indexOf('(') >= 0; 157 if (qualifiers != null) { 158 for (String qualifier : qualifiers) { 159 levels.add(qualifierLevel(qualifier, service)); 160 paren |= qualifier != null && qualifier.indexOf('(') >= 0; 161 } 162 } 163 levels.add(routineKey(routine, service)); 164 return new RoutineAliasKey(levels, null, signature, storedSignature(signature, service), paren, 165 qualifierKey(routine, service)); 166 } 167 168 /** 169 * The key of a routine name given as legacy normal-name text ({@code SCHEMA.NAME(suffix)}), for the deprecated 170 * text-keyed API of {@code ModelBindingManager} only. That text is already a stored spelling (the legacy maps 171 * compared it as stored text: {@code foo} there is not {@code FOO}), so its levels are keyed as stored names: the text 172 * before its last {@code (} (when it ends with {@code )}) is split into levels, and the rest is the alias suffix. 173 * Null when it names nothing. 174 */ 175 public static RoutineAliasKey ofLegacyText(String text) { 176 return ofLegacyText(text, service()); 177 } 178 179 /** {@link #ofLegacyText(String)} under the given analysis's identifier rules. */ 180 public static RoutineAliasKey ofLegacyText(String text, IdentifierService service) { 181 if (text == null || text.isEmpty() || service == null) { 182 return null; 183 } 184 String name = text; 185 String signature = null; 186 int cut = text.lastIndexOf('('); 187 if (text.endsWith(")") && cut > 0) { 188 name = text.substring(0, cut); 189 signature = text.substring(cut); 190 } 191 try { 192 List<String> segments = SQLUtil.parseNames(name); 193 if (segments == null || segments.isEmpty()) { 194 return null; 195 } 196 List<Object> levels = new ArrayList<Object>(segments.size()); 197 boolean paren = false; 198 for (int i = 0; i < segments.size(); i++) { 199 String segment = segments.get(i); 200 if (segment == null || segment.isEmpty()) { 201 return null; 202 } 203 levels.add(service.canonKeyOf(segment, i == segments.size() - 1 ? ROUTINE_TYPE : QUALIFIER_TYPE, 204 IdentifierInputForm.CATALOG_STORED)); 205 paren |= segment.indexOf('(') >= 0; 206 } 207 String routine = segments.get(segments.size() - 1); 208 return new RoutineAliasKey(levels, null, signature, storedSignature(signature, service), paren, 209 service.canonKeyOf(routine, QUALIFIER_TYPE, IdentifierInputForm.CATALOG_STORED)); 210 } catch (RuntimeException unkeyable) { 211 return null; 212 } 213 } 214 215 /** 216 * A qualifier level. The empty one of {@code db..p} is SQL Server's default schema there, as the legacy normalizer 217 * read it (the session's schema, else {@code dbo}); elsewhere it stays an empty level. 218 */ 219 private static Object qualifierLevel(String text, IdentifierService service) { 220 if (text != null && !text.isEmpty()) { 221 return qualifierKey(text, service); 222 } 223 EDbVendor vendor = service.getProfile() == null ? null : service.getProfile().getVendor(); 224 if (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) { 225 String schema = ModelBindingManager.getGlobalSchema(); 226 return qualifierKey(schema != null ? schema : "dbo", service); 227 } 228 return EMPTY_LEVEL; 229 } 230 231 static CanonKey qualifierKey(String text, IdentifierService service) { 232 return service.canonKeyOf(text, QUALIFIER_TYPE, IdentifierInputForm.SQL_LEXICAL); 233 } 234 235 static CanonKey routineKey(String text, IdentifierService service) { 236 return service.canonKeyOf(text, ROUTINE_TYPE, IdentifierInputForm.SQL_LEXICAL); 237 } 238 239 /** 240 * The alias suffix as the legacy leaf comparison saw it: part of one stored routine name, compared by the 241 * routine rules (so {@code (MyType)} and {@code (mytype)} are one suffix where routine names ignore case). 242 */ 243 private static CanonKey storedSignature(String signature, IdentifierService service) { 244 if (signature == null || signature.isEmpty()) { 245 return null; 246 } 247 return service.canonKeyOf(signature, ROUTINE_TYPE, IdentifierInputForm.CATALOG_STORED); 248 } 249 250 /** 251 * The key of a model's display name ({@code Procedure}, {@code OraclePackage}), built beside the composition that 252 * renders it, branch for branch: the composition appends written segments and filled-in database or schema 253 * values, and this appends the matching levels. A written segment's level is the written name's own level when the 254 * name's levels line up with the composition's segments; a filled-in value is keyed as one identifier. 255 */ 256 public static final class Display { 257 private final RoutineAliasKey written; 258 private final List<String> segments; 259 private final IdentifierService service; 260 private final List<Object> levels = new ArrayList<Object>(); 261 private boolean paren; 262 private boolean failed; 263 264 public Display(TObjectName name, List<String> segments) { 265 this.service = service(); 266 RoutineAliasKey key = null; 267 try { 268 key = written(name, null, service); 269 } catch (RuntimeException failure) { 270 failed = true; 271 } 272 this.written = key != null && segments != null && key.size() == segments.size() ? key : null; 273 this.segments = segments; 274 if (service == null) { 275 failed = true; 276 } 277 } 278 279 /** The composition appended written segment {@code index} as a qualifier. */ 280 public void segment(int index) { 281 if (failed) { 282 return; 283 } 284 try { 285 levels.add(written != null ? written.levels.get(index) : qualifierLevel(segments.get(index), service)); 286 paren |= segments.get(index) != null && segments.get(index).indexOf('(') >= 0; 287 } catch (RuntimeException failure) { 288 failed = true; 289 } 290 } 291 292 /** The composition appended a filled-in database or schema value. */ 293 public void field(String value) { 294 if (failed) { 295 return; 296 } 297 try { 298 levels.add(qualifierLevel(value, service)); 299 paren |= value != null && value.indexOf('(') >= 0; 300 } catch (RuntimeException failure) { 301 failed = true; 302 } 303 } 304 305 /** The composition appended the routine's own (last) segment: the key of the display name, or null. */ 306 public RoutineAliasKey finish() { 307 if (failed) { 308 return null; 309 } 310 try { 311 if (segments == null || segments.isEmpty()) { 312 return null; 313 } 314 String last = segments.get(segments.size() - 1); 315 paren |= last != null && last.indexOf('(') >= 0; 316 if (written != null) { 317 levels.add(written.getRoutineKey()); 318 return new RoutineAliasKey(levels, written.dblink, null, null, paren, written.routineAsQualifier); 319 } 320 levels.add(routineKey(last, service)); 321 return new RoutineAliasKey(levels, null, null, null, paren, qualifierKey(last, service)); 322 } catch (RuntimeException failure) { 323 return null; 324 } 325 } 326 } 327 328 /** This key with {@code qualifiers}' levels in front, for a member of the package {@code qualifiers} names. */ 329 public RoutineAliasKey qualifiedBy(RoutineAliasKey qualifiers) { 330 if (qualifiers == null) { 331 return this; 332 } 333 List<Object> joined = new ArrayList<Object>(qualifiers.levels.size() + levels.size()); 334 joined.addAll(qualifiers.levels.subList(0, qualifiers.levels.size() - 1)); 335 joined.add(qualifiers.routineAsQualifier); 336 joined.addAll(levels); 337 return new RoutineAliasKey(joined, dblink, signature, storedSignature, parenInName || qualifiers.parenInName, 338 routineAsQualifier); 339 } 340 341 /** This key's levels with another alias suffix. */ 342 public RoutineAliasKey withSignature(String otherSignature) { 343 return withSignature(otherSignature, service()); 344 } 345 346 public RoutineAliasKey withSignature(String otherSignature, IdentifierService service) { 347 if (otherSignature == null ? signature == null : otherSignature.equals(signature)) { 348 return this; 349 } 350 return new RoutineAliasKey(new ArrayList<Object>(levels), dblink, otherSignature, 351 service == null ? null : storedSignature(otherSignature, service), parenInName, routineAsQualifier); 352 } 353 354 /** This key without an alias suffix: the name alone. */ 355 public RoutineAliasKey nameOnly() { 356 return signature == null ? this 357 : new RoutineAliasKey(new ArrayList<Object>(levels), dblink, null, null, parenInName, routineAsQualifier); 358 } 359 360 public int size() { 361 return levels.size(); 362 } 363 364 public boolean isQualified() { 365 return levels.size() > 1; 366 } 367 368 public boolean hasSignature() { 369 return signature != null; 370 } 371 372 /** Whether the legacy lookup took this key's text for a name with a suffix: it has one, or a {@code (} as written. */ 373 public boolean looksSuffixed() { 374 return signature != null || parenInName; 375 } 376 377 /** 378 * This key with {@code qualifier} as written in front, a single identifier (a model's schema field); for a lookup 379 * the legacy made by prefixing that text. 380 */ 381 public RoutineAliasKey withQualifier(String qualifier) { 382 return withQualifier(qualifier, service()); 383 } 384 385 public RoutineAliasKey withQualifier(String qualifier, IdentifierService service) { 386 if (qualifier == null || service == null) { 387 return this; 388 } 389 List<Object> joined = new ArrayList<Object>(levels.size() + 1); 390 joined.add(qualifierLevel(qualifier, service)); 391 joined.addAll(levels); 392 return new RoutineAliasKey(joined, dblink, signature, storedSignature, 393 parenInName || qualifier.indexOf('(') >= 0, routineAsQualifier); 394 } 395 396 /** The alias suffix as raw text, or null. */ 397 public String getSignature() { 398 return signature; 399 } 400 401 /** The routine level's key. */ 402 public CanonKey getRoutineKey() { 403 return (CanonKey) levels.get(levels.size() - 1); 404 } 405 406 /** The name's levels (a {@code CanonKey}, or the empty level), outermost first. */ 407 public List<Object> getLevels() { 408 return levels; 409 } 410 411 boolean sameName(RoutineAliasKey other) { 412 return levels.equals(other.levels) && sameLink(other); 413 } 414 415 boolean sameRoutine(RoutineAliasKey other) { 416 return getRoutineKey().equals(other.getRoutineKey()) && sameLink(other); 417 } 418 419 private boolean sameLink(RoutineAliasKey other) { 420 return dblink == null ? other.dblink == null : dblink.equals(other.dblink); 421 } 422 423 /** The legacy {@code startsWith} of the full key text, once the names are equal: the suffix as raw text. */ 424 boolean signatureStartsWith(RoutineAliasKey other) { 425 return signature != null && other.signature != null && signature.startsWith(other.signature); 426 } 427 428 /** The legacy leaf comparison's suffix part: both suffixes, as stored routine-name text. */ 429 boolean sameStoredSignature(RoutineAliasKey other) { 430 if (signature == null || other.signature == null) { 431 return signature == null && other.signature == null; 432 } 433 if (storedSignature == null || other.storedSignature == null) { 434 return signature.equals(other.signature); 435 } 436 return storedSignature.equals(other.storedSignature); 437 } 438 439 @Override 440 public boolean equals(Object o) { 441 if (this == o) { 442 return true; 443 } 444 if (!(o instanceof RoutineAliasKey)) { 445 return false; 446 } 447 RoutineAliasKey that = (RoutineAliasKey) o; 448 return hash == that.hash && levels.equals(that.levels) && sameLink(that) 449 && (signature == null ? that.signature == null : signature.equals(that.signature)); 450 } 451 452 @Override 453 public int hashCode() { 454 return hash; 455 } 456 457 /** For diagnostics only: never compare or key by this text. */ 458 @Override 459 public String toString() { 460 StringBuilder sb = new StringBuilder(); 461 for (int i = 0; i < levels.size(); i++) { 462 if (i > 0) { 463 sb.append('.'); 464 } 465 Object level = levels.get(i); 466 sb.append(level instanceof CanonKey ? ((CanonKey) level).getCanonText() : String.valueOf(level)); 467 } 468 if (dblink != null) { 469 sb.append('@').append(dblink.getCanonText()); 470 } 471 if (signature != null) { 472 sb.append(signature); 473 } 474 return sb.toString(); 475 } 476}