001package gudusoft.gsqlparser.dlineage.util; 002 003import java.io.File; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.LinkedHashSet; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010 011import gudusoft.gsqlparser.EDbVendor; 012import gudusoft.gsqlparser.EParameterMode; 013import gudusoft.gsqlparser.TCustomSqlStatement; 014import gudusoft.gsqlparser.dlineage.dataflow.model.AnalyzeMode; 015import gudusoft.gsqlparser.dlineage.dataflow.model.Column; 016import gudusoft.gsqlparser.dlineage.dataflow.model.ModelBindingManager; 017import gudusoft.gsqlparser.dlineage.dataflow.model.Option; 018import gudusoft.gsqlparser.dlineage.dataflow.model.SqlInfo; 019import gudusoft.gsqlparser.dlineage.dataflow.model.SubType; 020import gudusoft.gsqlparser.dlineage.dataflow.model.Table; 021import gudusoft.gsqlparser.dlineage.dataflow.model.xml.*; 022import gudusoft.gsqlparser.nodes.TFunctionCall; 023import gudusoft.gsqlparser.nodes.TObjectName; 024import gudusoft.gsqlparser.nodes.TParameterDeclaration; 025import gudusoft.gsqlparser.nodes.TParameterDeclarationList; 026import gudusoft.gsqlparser.sqlenv.IdentifierService; 027import gudusoft.gsqlparser.sqlenv.IdentifierCodec; 028import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 029import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 030import gudusoft.gsqlparser.sqlenv.QualifiedCanonKey; 031import gudusoft.gsqlparser.sqlenv.QualifiedNameHierarchy; 032import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 033import gudusoft.gsqlparser.sqlenv.TSQLEnv; 034import gudusoft.gsqlparser.stmt.TCommonBlock; 035import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 036import gudusoft.gsqlparser.stmt.TCreateFunctionStmt; 037import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement; 038import gudusoft.gsqlparser.stmt.mssql.TMssqlBlock; 039import gudusoft.gsqlparser.stmt.mssql.TMssqlCreateFunction; 040import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage; 041import gudusoft.gsqlparser.TBaseType; 042import gudusoft.gsqlparser.util.SQLUtil; 043import gudusoft.gsqlparser.stmt.teradata.TTeradataCreateProcedure; 044import gudusoft.gsqlparser.util.SQLUtil; 045import gudusoft.gsqlparser.util.functionChecker; 046import gudusoft.gsqlparser.util.keywordChecker; 047import gudusoft.gsqlparser.util.json.JSON; 048 049public class DlineageUtil { 050 051 /** 052 * Whether a serialized coordinate ("[line,col,offset],[line,col,offset]...") 053 * points at a real reference site. A position synthesized for a column that 054 * has no source token renders as "[1,1,0],[1,n,0]" (start-of-module, width = 055 * name length) or carries -1 markers; a genuine column reference can never 056 * start at line 1, column 1, because the statement head keyword occupies 057 * that position. 058 */ 059 public static boolean hasUsableCoordinate(String coordinate) { 060 if (SQLUtil.isEmpty(coordinate)) { 061 return false; 062 } 063 String trimmed = coordinate.trim(); 064 return !trimmed.startsWith("[1,1,") && !trimmed.startsWith("[-1,-1,"); 065 } 066 067 public static boolean compareIdentifier(String source, String target, ESQLDataObjectType sqlDataObjectType) { 068 return SQLUtil.compareIdentifier(ModelBindingManager.getGlobalVendor(), sqlDataObjectType, source, target); 069 } 070 071 public static boolean compareColumnIdentifier(String source, String target) { 072 return SQLUtil.compareIdentifier(ModelBindingManager.getGlobalVendor(), ESQLDataObjectType.dotColumn, source, 073 target); 074 } 075 076 public static boolean compareTableIdentifier(String source, String target) { 077 return SQLUtil.compareIdentifier(ModelBindingManager.getGlobalVendor(), ESQLDataObjectType.dotTable, source, 078 target); 079 } 080 081 /** 082 * Whether the analysis runs under identifier rules of its own rather than its vendor's 083 * defaults; only then do lineage name comparisons and keys follow the analysis's service. 084 */ 085 public static boolean configuresIdentifierRules() { 086 return configuredIdentifierService() != null; 087 } 088 089 /** 090 * Single-segment identifier equality via the unified façade, using the bound 091 * global analysis vendor; falls back to case-insensitive comparison when no 092 * vendor is bound (model classes used outside an analysis run). 093 */ 094 public static boolean sameName(ESQLDataObjectType objectType, String a, String b) { 095 if (a == null || b == null) { 096 return false; 097 } 098 IdentifierService service = configuredIdentifierService(); 099 if (service != null) { 100 return service.areEqual(a, b, objectType); 101 } 102 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 103 return vendor == null ? a.equalsIgnoreCase(b) : SQLUtil.sameName(vendor, objectType, a, b); 104 } 105 106 /** 107 * Like {@link #sameName} but for possibly-qualified names; routes through the 108 * segmenting {@link SQLUtil#compareIdentifier}. 109 */ 110 public static boolean sameQualifiedName(ESQLDataObjectType objectType, String a, String b) { 111 if (a == null || b == null) { 112 return false; 113 } 114 // Reflexivity guard: degenerate model names (e.g. the literal ".") defeat 115 // compareIdentifier's segmentation, but identical strings are always the same name. 116 if (a.equals(b)) { 117 return true; 118 } 119 IdentifierService service = configuredIdentifierService(); 120 if (service != null) { 121 return SQLUtil.compareIdentifier(service, objectType, withSessionSchema(a), withSessionSchema(b)); 122 } 123 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 124 return vendor == null ? a.equalsIgnoreCase(b) // non-identifier-compare: no analysis vendor bound 125 : SQLUtil.compareIdentifier(vendor, objectType, withSessionSchema(a), withSessionSchema(b)); 126 } 127 128 /** 129 * The identity key of a table name, or of a table alias, AS WRITTEN IN SQL: a 130 * {@link QualifiedCanonKey} under the analysis's identifier rules (its configured profile, else its 131 * vendor's defaults), with an omitted SQL Server schema filled in as {@link #withSessionSchema} 132 * does. Text that is not a qualified identifier (a database link written into the name, a 133 * function-call shape) is keyed exactly, as a label, so it never merges with another spelling. 134 * 135 * <p>Only for text the SQL spelled. A model's display name has lost its quote state and must not 136 * be keyed here.</p> 137 */ 138 public static Object tableNameKey(String lexical) { 139 if (lexical == null) { 140 return null; 141 } 142 String name = withSessionSchema(lexical); 143 IdentifierService service = ModelBindingManager.getGlobalIdentifierService(); 144 if (service != null && !name.isEmpty()) { 145 try { 146 QualifiedCanonKey key = service.qualifiedCanonKeyFromLexical(name, ESQLDataObjectType.dotTable); 147 if (key != null) { 148 return key; 149 } 150 } catch (RuntimeException notAQualifiedName) { 151 // keyed exactly below 152 } 153 } 154 return gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.of( 155 gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.Kind.SYNTHETIC, name); 156 } 157 158 /** 159 * The identity key of a schema name AS WRITTEN IN SQL ({@code CREATE SCHEMA s CLONE db.src}): a 160 * {@link QualifiedCanonKey} of {@code dotSchema} under the analysis's identifier rules, its segments the 161 * catalog and the schema. Text the rules cannot read as a name is keyed exactly, as a label, so it never 162 * merges with another spelling. 163 */ 164 public static Object schemaNameKey(String lexical) { 165 return analysisNameKey(lexical, ESQLDataObjectType.dotSchema); 166 } 167 168 /** 169 * The identity key of a name AS WRITTEN IN SQL in the role {@code objectType} ({@code dotCatalog} for the 170 * database of {@code CREATE DATABASE d CLONE src}): a {@link QualifiedCanonKey} under the analysis's identifier 171 * rules. Text the rules cannot read as a name is keyed exactly, as a label, so it never merges with another 172 * spelling. 173 */ 174 public static Object analysisNameKey(String lexical, ESQLDataObjectType objectType) { 175 if (lexical == null) { 176 return null; 177 } 178 IdentifierService service = ModelBindingManager.getGlobalIdentifierService(); 179 if (service != null && !lexical.isEmpty()) { 180 try { 181 QualifiedCanonKey key = service.qualifiedCanonKeyFromLexical(lexical, objectType); 182 if (key != null) { 183 return key; 184 } 185 } catch (RuntimeException notAQualifiedName) { 186 // keyed exactly below 187 } 188 } 189 return gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.of( 190 gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.Kind.SYNTHETIC, lexical); 191 } 192 193 /** 194 * A SQL Server name may omit its schema ({@code db..t}); the omission means the session's 195 * default schema, a fact about the analysis rather than about the name. Every interior empty 196 * segment is filled with it: the schema the analysis is in, else {@code dbo}. This is the 197 * qualification lineage has always applied before comparing such names. Segments are split by 198 * {@link SQLUtil#parseNames}, so a delimited segment such as {@code [a..b]} is never looked into. 199 * 200 * @return the name with its omitted schema filled in, or the name unchanged for any other vendor 201 */ 202 public static String withSessionSchema(String qualifiedName) { 203 return withSessionSchema(ModelBindingManager.getGlobalVendor(), qualifiedName); 204 } 205 206 /** {@link #withSessionSchema(String)} for a vendor given explicitly. */ 207 public static String withSessionSchema(EDbVendor vendor, String qualifiedName) { 208 if (qualifiedName == null || (vendor != EDbVendor.dbvmssql && vendor != EDbVendor.dbvazuresql) 209 || qualifiedName.indexOf('.') == -1) { 210 return qualifiedName; 211 } 212 List<String> segments = SQLUtil.parseNames(qualifiedName); 213 boolean filled = false; 214 for (int i = 1; i < segments.size() - 1; i++) { 215 if (segments.get(i).isEmpty() && !segments.get(i - 1).isEmpty()) { 216 String schema = ModelBindingManager.getGlobalSchema(); 217 segments.set(i, schema != null ? schema : "dbo"); 218 filled = true; 219 } 220 } 221 return filled ? String.join(".", segments) : qualifiedName; 222 } 223 224 /** 225 * Canonical equality of the column (last) segments of two references — the 226 * canonical counterpart of comparing {@link #getColumnName(String)} results: 227 * same naive last-dot split, same trim and empty-segment fallback, but the raw 228 * segments go to the canonical engine instead of normalize-then-equalsIgnoreCase. 229 */ 230 public static boolean sameColumnSegment(String a, String b) { 231 return sameName(ESQLDataObjectType.dotColumn, columnSegmentOf(a), columnSegmentOf(b)); 232 } 233 234 /** 235 * Whether two column NORMAL names (results of {@link #getIdentifierNormalColumnName}) name the 236 * same column. Lineage has always matched normal names case-insensitively; this keeps that and 237 * only narrows it, so two names that differ just in case match only when the identifier rules 238 * say they are one name. ClickHouse {@code userId} and {@code userid}, which 239 * {@link NormalNameCollisions} gives normal names of their own, stay apart. 240 */ 241 public static boolean sameColumnNormalName(String a, String b) { 242 if (a == null || b == null) { 243 return false; 244 } 245 if (a.equals(b)) { 246 return true; 247 } 248 // The case-insensitive match lineage always made, narrowed by the identifier rules. 249 return a.equalsIgnoreCase(b) && sameName(ESQLDataObjectType.dotColumn, a, b); // non-identifier-compare: legacy normal names, the rules decide 250 } 251 252 /** 253 * Whether a model column's name and a reference name DISPLAY alike: their column segments in the form output 254 * version 1 publishes ({@link LegacyDlineageWireNamesV1#columnName}), compared as the vendor compares names. This 255 * is a comparison of displays, not of identities, for the places that compare names where identity cannot decide: 256 * star links grouped by display ({@code -Dgsp.dlineage.starLinks=legacy}), a label involved, a model name that 257 * carries no identity. A model name is display text ({@link gudusoft.gsqlparser.dlineage.dataflow.model.Column#getName}), 258 * so reading it as a SQL spelling would compare the wrong thing. 259 */ 260 public static boolean sameColumnDisplay(String modelName, TObjectName name) { 261 return compareColumnIdentifier(LegacyDlineageWireNamesV1.columnName(columnSegmentOf(modelName)), 262 LegacyDlineageWireNamesV1.columnName(name)); 263 } 264 265 /** {@link #sameColumnDisplay(String, TObjectName)} of two model names, each cut at its last dot. */ 266 public static boolean sameColumnDisplay(String modelName, String otherName) { 267 return compareColumnIdentifier(LegacyDlineageWireNamesV1.columnName(columnSegmentOf(modelName)), 268 LegacyDlineageWireNamesV1.columnName(columnSegmentOf(otherName))); 269 } 270 271 /** Canonical counterpart of comparing {@link #getColumnName(TObjectName)} results. */ 272 public static boolean sameColumnName(TObjectName a, TObjectName b) { 273 return sameName(ESQLDataObjectType.dotColumn, rawColumnName(a), rawColumnName(b)); 274 } 275 276 /** Mixed operand shapes: model name string vs AST object name. */ 277 public static boolean sameColumnName(String a, TObjectName b) { 278 return sameName(ESQLDataObjectType.dotColumn, columnSegmentOf(a), rawColumnName(b)); 279 } 280 281 /** Mixed operand shapes: AST object name vs model name string. */ 282 public static boolean sameColumnName(TObjectName a, String b) { 283 return sameColumnName(b, a); 284 } 285 286 /** Both operands model name strings: see {@link #sameColumnSegment}. */ 287 public static boolean sameColumnName(String a, String b) { 288 return sameColumnSegment(a, b); 289 } 290 291 /** 292 * Whether two column paths name the same column: the same number of segments, each segment the 293 * same COLUMN name. A path here is a column and the fields below it (a BigQuery or Redshift 294 * STRUCT path such as {@code customer.name}), so no segment is a table or schema qualifier and 295 * none follows the rules of one: on BigQuery tables are case-sensitive while columns and fields 296 * are not. For a reference that may carry a table qualifier use {@link #sameQualifiedName}. 297 */ 298 public static boolean sameColumnPath(String a, String b) { 299 if (a == null || b == null) { 300 return false; 301 } 302 if (a.equals(b)) { 303 return true; 304 } 305 List<String> left = SQLUtil.parseNames(a); 306 List<String> right = SQLUtil.parseNames(b); 307 if (left.size() != right.size()) { 308 return false; 309 } 310 for (int i = 0; i < left.size(); i++) { 311 if (!left.get(i).equals(right.get(i)) 312 && !sameName(ESQLDataObjectType.dotColumn, left.get(i), right.get(i))) { 313 return false; 314 } 315 } 316 return true; 317 } 318 319 /** {@link #sameColumnPath(String, String)} for a vendor given explicitly, under its default rules. */ 320 public static boolean sameColumnPath(EDbVendor vendor, String a, String b) { 321 if (a == null || b == null) { 322 return false; 323 } 324 if (a.equals(b)) { 325 return true; 326 } 327 List<String> left = SQLUtil.parseNames(a); 328 List<String> right = SQLUtil.parseNames(b); 329 if (left.size() != right.size()) { 330 return false; 331 } 332 for (int i = 0; i < left.size(); i++) { 333 if (!left.get(i).equals(right.get(i)) 334 && !SQLUtil.sameName(vendor, ESQLDataObjectType.dotColumn, left.get(i), right.get(i))) { 335 return false; 336 } 337 } 338 return true; 339 } 340 341 /** 342 * Whether a column model is the column a reference names. The model's own identity key, minted 343 * from the spelling it was created with, decides whenever both sides have one: the model's name is 344 * a display string (delimiters decoded where they are cosmetic) and must not be re-read as SQL. A 345 * model without one (a STRUCT path, an expression label, a wildcard) is matched by its name as a 346 * column path against the reference's column segment, as written. 347 */ 348 public static boolean columnMatches(Column model, TObjectName reference) { 349 if (model == null || reference == null) { 350 return false; 351 } 352 if (model.getIdentifierKey() != null) { 353 gudusoft.gsqlparser.sqlenv.CanonKey key = columnKey(reference); 354 if (key != null) { 355 return model.getIdentifierKey().equals(key); 356 } 357 } 358 return sameColumnPath(model.getName(), rawColumnName(reference)); 359 } 360 361 /** {@link #columnMatches(Column, TObjectName)} for a reference held as text, as written. */ 362 public static boolean columnMatches(Column model, String reference) { 363 if (model == null || reference == null) { 364 return false; 365 } 366 String segment = columnSegmentOf(reference); 367 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 368 if (model.getIdentifierKey() != null && vendor != null) { 369 gudusoft.gsqlparser.sqlenv.CanonKey key; 370 try { 371 key = lexicalColumnKey(vendor, segment); 372 } catch (RuntimeException notAnIdentifier) { 373 key = null; 374 } 375 if (key != null) { 376 return model.getIdentifierKey().equals(key); 377 } 378 } 379 return sameColumnPath(model.getName(), segment); 380 } 381 382 /** 383 * The RAW column (last) segment of a reference — {@link #getColumnName(String)}'s 384 * extraction (naive last-dot split, trim, whole-string fallback) WITHOUT the 385 * normalization, for callers that canonically compare a segment against a 386 * differently-shaped operand. 387 */ 388 public static String columnSegmentOf(String column) { 389 if (column == null) { 390 return null; 391 } 392 String name = column.substring(column.lastIndexOf('.') + 1); 393 if ("".equals(name.trim())) { 394 return column.trim(); 395 } 396 return name.trim(); 397 } 398 399 /** The column (last) segment of an AST name as written: {@link #getColumnName(TObjectName)} without the normalization. */ 400 public static String rawColumnName(TObjectName column) { 401 if (column == null || column.toString() == null) { 402 return null; 403 } 404 String name = column.getColumnNameOnly(); 405 if (name == null || "".equals(name.trim())) { 406 return column.toString().trim(); 407 } 408 return name.trim(); 409 } 410 411 /** Identity comes from the AST spelling, never a rendered model name. */ 412 public static gudusoft.gsqlparser.sqlenv.CanonKey columnKey(TObjectName column) { 413 if (column == null || isWildcard(column) || column.getPropertyToken() != null 414 || column.getDbObjectType() == gudusoft.gsqlparser.EDbObjectType.variable 415 || column.getDbObjectType() == gudusoft.gsqlparser.EDbObjectType.parameter 416 || (column.getColumnSource() != null && column.getColumnSource().isStructFieldAccess())) { 417 return null; 418 } 419 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 420 return vendor == null ? null : lexicalColumnKey(vendor, rawColumnName(column)); 421 } 422 423 /** 424 * Identity key of a column name as written in SQL: under the analysis's identifier rules 425 * when it configures its own, else under the vendor default, as it always was. Every 426 * producer of a lineage column key goes through here or {@link #catalogColumnKey}, because a 427 * key minted under one profile never equals a key minted under another. 428 */ 429 public static gudusoft.gsqlparser.sqlenv.CanonKey lexicalColumnKey(EDbVendor vendor, String lexical) { 430 IdentifierService service = configuredIdentifierService(vendor); 431 if (service == null) { 432 return SQLUtil.canonKey(vendor, ESQLDataObjectType.dotColumn, lexical); 433 } 434 return lexical == null ? null 435 : service.canonKeyOf(lexical, ESQLDataObjectType.dotColumn, IdentifierInputForm.SQL_LEXICAL); 436 } 437 438 /** 439 * Identity key of a name as written in SQL, qualified or not, under the same rules as {@link #lexicalColumnKey} 440 * (the analysis's rules when it configures its own for this vendor, else the vendor's defaults): a {@link QualifiedCanonKey} of 441 * {@code objectType}. Text the rules cannot read as a name is keyed exactly, as a label, so it never merges with 442 * another spelling. For readers whose input names are SQL spellings. 443 */ 444 public static Object lexicalNameKey(EDbVendor vendor, String lexical, ESQLDataObjectType objectType) { 445 if (lexical == null) { 446 return null; 447 } 448 IdentifierService service = configuredIdentifierService(vendor); 449 if (service == null && vendor != null) { 450 service = IdentifierService.defaultServiceFor(vendor); 451 } 452 if (service != null && !lexical.isEmpty()) { 453 try { 454 QualifiedCanonKey key = service.qualifiedCanonKeyFromLexical(lexical, objectType); 455 if (key != null) { 456 return key; 457 } 458 } catch (RuntimeException notAQualifiedName) { 459 // keyed exactly below 460 } 461 } 462 return gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.of( 463 gudusoft.gsqlparser.dlineage.dataflow.model.ModelLabelKey.Kind.SYNTHETIC, lexical); 464 } 465 466 /** Identity key of a column name as a catalog stores it; see {@link #lexicalColumnKey}. */ 467 public static gudusoft.gsqlparser.sqlenv.CanonKey catalogColumnKey(EDbVendor vendor, String storedName) { 468 IdentifierService service = configuredIdentifierService(vendor); 469 if (service == null) { 470 return SQLUtil.canonKey(vendor, ESQLDataObjectType.dotColumn, 471 TSQLEnv.toLexical(vendor, ESQLDataObjectType.dotColumn, storedName, IdentifierInputForm.CATALOG_STORED)); 472 } 473 return storedName == null ? null 474 : service.canonKeyOf(storedName, ESQLDataObjectType.dotColumn, IdentifierInputForm.CATALOG_STORED); 475 } 476 477 /** A stored column name spelled as SQL, under the same rules as {@link #catalogColumnKey}. */ 478 public static String catalogColumnLexical(EDbVendor vendor, String storedName) { 479 IdentifierService service = configuredIdentifierService(vendor); 480 return service == null 481 ? TSQLEnv.toLexical(vendor, ESQLDataObjectType.dotColumn, storedName, IdentifierInputForm.CATALOG_STORED) 482 : IdentifierCodec.encodeStored(service.getProfile(), ESQLDataObjectType.dotColumn, storedName); 483 } 484 485 /** 486 * A {@code *} dlineage synthesizes for a whole-table pseudo column (a clone, a rename, a synonym, a COPY target 487 * without columns). It has no part token, so the parse's own wildcard test ({@link #isWildcard}) does not take it 488 * for a SQL wildcard; {@link #isStarName} knows it by its class. 489 */ 490 public static TObjectName syntheticStarName() { 491 return new SyntheticStarName(); 492 } 493 494 /** 495 * Whether a name stands for a star: the parse's wildcard ({@code *}, {@code t.*}), or a star dlineage 496 * synthesized ({@link #syntheticStarName}). Decided by the parse and by provenance, never by the text: a 497 * delimited column named {@code *}, and a name built from the text {@code *}, are not stars. 498 */ 499 public static boolean isStarName(TObjectName name) { 500 return isWildcard(name) || name instanceof SyntheticStarName; 501 } 502 503 /** 504 * The identity of the last segment of a field path (a struct or property access such as {@code a.left.Name}), 505 * the name lineage publishes for such a column. {@link #columnKey} gives a field path no identity of its own; 506 * this is the leaf's, under the analysis's rules, for questions about the published leaf name. Null for a name 507 * that is not a field path. 508 */ 509 public static gudusoft.gsqlparser.sqlenv.CanonKey fieldLeafKey(TObjectName name) { 510 if (name == null || isWildcard(name)) { 511 return null; 512 } 513 if (!isFieldPath(name)) { 514 return null; 515 } 516 String leaf = fieldLeafText(name); 517 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 518 return vendor == null || leaf == null || leaf.trim().isEmpty() ? null : lexicalColumnKey(vendor, leaf.trim()); 519 } 520 521 /** 522 * Whether lineage models a STRUCT or ROW column as a tree, a declared column with a node for each field path 523 * below it (fields, array elements, and the intermediate fields too), and binds a read of a field to that node: 524 * BigQuery and the Trino family (Athena, Presto, Trino). The dialects differ only in how a name is read as a 525 * column and a field path, which {@link TObjectName#getFieldPathRead()} answers. See 526 * gsp_java_core/doc/refactor/bigquery_nested_column_lineage_plan.md. 527 * 528 * @deprecated read {@code NestedAccessProfile.forVendor(vendor).buildsDeclaredTree()} 529 */ 530 @Deprecated 531 public static boolean modelsStructTree(EDbVendor vendor) { 532 return gudusoft.gsqlparser.nodes.structured.NestedAccessProfile.forVendor(vendor).buildsDeclaredTree(); 533 } 534 535 /** 536 * {@link #modelsStructTree(EDbVendor)} for the vendor of the analysis. 537 * 538 * @deprecated read {@code NestedAccessProfile.buildsDeclaredTree()} of the analysis's vendor 539 */ 540 @Deprecated 541 public static boolean modelsStructTree() { 542 return modelsStructTree(ModelBindingManager.getGlobalVendor()); 543 } 544 545 /** Whether a name reaches below its column: a property token, or a STRUCT field the resolver found. */ 546 public static boolean isFieldPath(TObjectName name) { 547 return name != null && (name.getPropertyToken() != null 548 || (name.getColumnSource() != null && name.getColumnSource().isStructFieldAccess())); 549 } 550 551 /** 552 * Whether two references name the same field below their column: neither is a field path, or both are and 553 * their leaves are the same name. {@link #sameColumnName(TObjectName, TObjectName)} compares the column segment 554 * only, so {@code tgt.objcol.a1} and {@code tgt.objcol.a2} are one column there. A lookup for the exact column 555 * a write lands in must check this as well, or the second attribute is written into the first (Mantis #4747). 556 */ 557 public static boolean sameFieldLeaf(TObjectName a, TObjectName b) { 558 if (a == null || b == null) { 559 return false; 560 } 561 boolean fieldPath = isFieldPath(a); 562 if (fieldPath != isFieldPath(b)) { 563 return false; 564 } 565 return !fieldPath || sameName(ESQLDataObjectType.dotColumn, fieldLeafText(a), fieldLeafText(b)); 566 } 567 568 /** 569 * The name of an attribute of a column, {@code column.attribute}, each segment decoded as a column name is: 570 * how a {@code TableColumn} for {@code tgt.objcol.attr} is named outside the STRUCT-path vendors, and how an 571 * UPDATE or MERGE action names the column it writes into such an attribute. Null when the name has no property 572 * token. 573 */ 574 public static String propertyColumnPath(TObjectName name) { 575 if (name == null || name.getPartToken() == null || name.getPropertyToken() == null) { 576 return null; 577 } 578 return decodeColumnIdentifier(name.getPartToken().getAstext()) + "." 579 + decodeColumnIdentifier(name.getPropertyToken().getAstext()); 580 } 581 582 /** 583 * The last segment of a field path as written: the last of the parts beyond the six the parser names 584 * ({@code a.b.c.d.e.f.Name}), else the resolver's field path's last segment, else the property token, else the 585 * column segment. The one extractor for a field path's published leaf and for its identity. 586 */ 587 public static String fieldLeafText(TObjectName name) { 588 if (name == null) { 589 return null; 590 } 591 java.util.List<gudusoft.gsqlparser.TSourceToken> parts = name.getAdditionalParts(); 592 if (parts != null && !parts.isEmpty()) { 593 return parts.get(parts.size() - 1).getAstext(); 594 } 595 gudusoft.gsqlparser.resolver2.model.ColumnSource source = name.getColumnSource(); 596 if (source != null && source.isStructFieldAccess() && source.hasFieldPath() 597 && source.getFieldPath().getLast() != null) { 598 return source.getFieldPath().getLast(); 599 } 600 if (name.getPropertyToken() != null) { 601 return propertyLeafToken(name).getAstext(); 602 } 603 return rawColumnName(name); 604 } 605 606 /** 607 * The last field of a name that has a property token: the property itself, or, when the property is the first 608 * of several fields, the last of them. Athena {@code payload.address.city} is read as column {@code payload} 609 * with property {@code address}, and ends in the field {@code city} (Mantis 4822); the parse's identifier chain 610 * ({@link TObjectName#getIdentifierChain()}) has them all. Null for a name with no property token. 611 */ 612 public static gudusoft.gsqlparser.TSourceToken propertyLeafToken(TObjectName name) { 613 if (name == null || name.getPropertyToken() == null) { 614 return null; 615 } 616 java.util.List<gudusoft.gsqlparser.TSourceToken> chain = name.getIdentifierChain(); 617 for (int i = 0; i < chain.size() - 1; i++) { 618 if (chain.get(i) == name.getPropertyToken()) { 619 return chain.get(chain.size() - 1); 620 } 621 } 622 return name.getPropertyToken(); 623 } 624 625 /** 626 * Carries a model column's star provenance onto its published column: a column NAMED {@code *} that is not a 627 * star is marked {@code star="false"}, so the output, in memory and read back from XML, tells it from a star 628 * ({@link gudusoft.gsqlparser.dlineage.dataflow.model.xml.column#standsForStar()}). 629 */ 630 public static void publishStarProvenance(gudusoft.gsqlparser.dlineage.dataflow.model.xml.column element, 631 gudusoft.gsqlparser.dlineage.dataflow.model.Column model) { 632 if (element != null && model != null && !model.isStarColumn() && "*".equals(element.getName())) { 633 element.setStar(Boolean.FALSE); 634 } 635 } 636 637 /** 638 * Whether a relationship endpoint stands for a star: by its published column when the document has one under 639 * its id, else by the name the endpoint carries (an endpoint of a column the document does not list, such as a 640 * star-link entry, is named after it). 641 */ 642 public static boolean isStarEndpoint(java.util.Map<String, gudusoft.gsqlparser.dlineage.dataflow.model.xml.column> columnsById, 643 String id, String name) { 644 gudusoft.gsqlparser.dlineage.dataflow.model.xml.column element = columnsById == null || id == null ? null 645 : columnsById.get(id); 646 return element != null ? element.standsForStar() : "*".equals(name); 647 } 648 649 /** The published columns of a document by id, for {@link #isStarEndpoint}. */ 650 public static java.util.Map<String, gudusoft.gsqlparser.dlineage.dataflow.model.xml.column> outputColumnsById( 651 gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow document) { 652 java.util.Map<String, gudusoft.gsqlparser.dlineage.dataflow.model.xml.column> byId = 653 new java.util.HashMap<String, gudusoft.gsqlparser.dlineage.dataflow.model.xml.column>(); 654 if (document == null) { 655 return byId; 656 } 657 for (gudusoft.gsqlparser.dlineage.dataflow.model.xml.table t 658 : gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow.getAllTables(document)) { 659 if (t.getColumns() == null) { 660 continue; 661 } 662 for (gudusoft.gsqlparser.dlineage.dataflow.model.xml.column c : t.getColumns()) { 663 if (c.getId() != null && !byId.containsKey(c.getId())) { 664 byId.put(c.getId(), c); 665 } 666 } 667 } 668 return byId; 669 } 670 671 public static boolean isWildcard(TObjectName column) { 672 return column != null && column.getPartToken() != null 673 && column.getPartToken().tokentype == gudusoft.gsqlparser.ETokenType.ttasterisk; 674 } 675 676 /** 677 * Decode a delimited COLUMN identifier to the name a catalog stores, keeping 678 * the author's letter case: {@code [Volgorde]}, {@code "Volgorde"}, 679 * {@code `Volgorde`} and the SQL Server legacy alias form {@code 'Volgorde'} 680 * all become {@code Volgorde}; escapes resolve ({@code [Esc]]aped]} → 681 * {@code Esc]aped}). Vendor- and role-aware, so a spelling that is not a 682 * delimiter for the analysis vendor is left alone. 683 * 684 * <p>Used for the column names dlineage PUBLISHES, so that view columns and 685 * relationship endpoints carry the same identifier a consumer finds in 686 * {@code sys.columns} instead of a syntactic spelling it has to strip 687 * itself (GitHub issue #704). Unrecognized or malformed input is returned 688 * unchanged. 689 * 690 * <p><b>Only call this on a string of identifier provenance</b> — a 691 * {@code TObjectName} column token or a {@code TAliasClause} alias name. 692 * A string CONSTANT reaches the model as quoted text too 693 * ({@code 'literal value'} becomes a result-set column name), and decoding 694 * one would corrupt the value it carries. 695 * 696 * <p><b>Nothing is decoded on a vendor whose delimiters carry identity.</b> 697 * Quoting is not always cosmetic: where a vendor case-folds UNQUOTED names, 698 * the delimiters are what mark a name as case-exact, so Oracle 699 * {@code "Purchase Frequency"} and {@code Purchase Frequency} (which folds 700 * to {@code PURCHASE FREQUENCY}) are two DIFFERENT columns, and decoding 701 * would rename the column rather than clean up its spelling. Such a vendor 702 * is skipped ENTIRELY, by {@link #delimitersAreCosmetic}, rather than 703 * name-by-name: {@code "DESCRIPTION"} happens to fold to itself and could be 704 * decoded safely, but decoding it while {@code "Mixed Case"} keeps its 705 * quotes would publish two spelling conventions in one Oracle document and 706 * leave consumers no single rule to apply. SQL Server — where {@code [x]}, 707 * {@code "x"}, {@code 'x'} and {@code x} are all one case-insensitive column 708 * — is cosmetic throughout, which is why issue #704 is a SQL Server issue. 709 * 710 * <p><b>A name whose payload contains a dot keeps its delimiters</b>, even 711 * on a vendor that passes the test above. Model names double as keys in the 712 * dot-delimited qualified-name domain that {@link SQLUtil#parseNames} 713 * segments, where the delimiters are what stop {@code "name.dot"} from 714 * reading as {@code name} qualified by {@code dot} (the invariant 715 * {@link SQLUtil#quoteDottedName} exists to keep). Publishing the decoded 716 * form there cost a real CTAS lineage edge in the Snowflake corpus — a 717 * downstream re-split renamed the target column to {@code dot} and dropped 718 * its dependency. 719 * 720 * <p>In both held-back cases the name is returned exactly as written, so a 721 * consumer still has to decode it; that is strictly better than dropping an 722 * edge or renaming a column to improve a spelling. 723 * 724 * @see SQLUtil#decodeDelimitedName 725 */ 726 public static String decodeColumnIdentifier(String name) { 727 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 728 if (name == null || vendor == null) { 729 // No bound analysis vendor (model classes used outside a run): which 730 // spellings delimit is unknowable, so leave the name as written. 731 return name; 732 } 733 if (!delimitersAreCosmetic(vendor)) { 734 return name; 735 } 736 String decoded = SQLUtil.decodeDelimitedName(vendor, ESQLDataObjectType.dotColumn, name); 737 if (decoded == null || decoded.equals(name) || !publishable(vendor, decoded)) { 738 return name; 739 } 740 return decoded; 741 } 742 743 /** 744 * Decode a delimited identifier that sits in ALIAS position. 745 * 746 * <p>Same policy as {@link #decodeColumnIdentifier}, plus the vendor-blind 747 * apostrophe form: several vendors accept a string literal as a column 748 * alias ({@code SELECT c AS 'myTime'} is legal MySQL), and in alias position 749 * there is no ambiguity with a value literal to protect against — a 750 * {@code TAliasClause} has already proved this token names a column. The 751 * column-reference codec cannot make that call, because for MySQL an 752 * apostrophe delimits a STRING, not an identifier. 753 * 754 * <p>The apostrophe branch also preserves what 755 * {@code SQLUtil.trimSingleQuote} did here before issue #704: without it, 756 * routing aliases through the column codec would REGRESS MySQL 757 * {@code AS 'myTime'} from {@code myTime} back to {@code 'myTime'}. 758 * The {@link #publishable} guards still apply, so this path can only ever 759 * publish a name that survives a downstream re-decode. 760 */ 761 public static String decodeAliasIdentifier(String alias) { 762 EDbVendor vendor = ModelBindingManager.getGlobalVendor(); 763 if (alias == null || vendor == null) { 764 return alias; 765 } 766 String decoded = decodeColumnIdentifier(alias); 767 if (!decoded.equals(alias)) { 768 return decoded; 769 } 770 String unquoted = SQLUtil.trimSingleQuote(alias); 771 if (unquoted.equals(alias) || !publishable(vendor, unquoted)) { 772 return alias; 773 } 774 // publishable() only knows the COLUMN codec, which on MySQL does not 775 // treat an apostrophe as a delimiter. An alias like '''foo''' strips to 776 // ''foo'', which THIS method would strip again on the next layer — so a 777 // chain of SELECT * views would rename the column at every hop. Require 778 // the alias decode to be idempotent too. 779 if (!SQLUtil.trimSingleQuote(unquoted).equals(unquoted)) { 780 return alias; 781 } 782 return unquoted; 783 } 784 785 /** 786 * Is {@code decoded} safe to PUBLISH as a model name, or would emitting it 787 * cost a lineage edge? Two ways a decoded payload becomes a liability: 788 * 789 * <ol> 790 * <li><b>It contains a dot.</b> Model names double as keys in the 791 * dot-delimited qualified-name domain that {@link SQLUtil#parseNames} 792 * segments, so {@code "name.dot"} decoded to {@code name.dot} reads as 793 * {@code name} qualified by {@code dot}. That cost a real Snowflake CTAS 794 * edge: a downstream re-split renamed the target column to {@code dot}.</li> 795 * <li><b>It is ITSELF a delimited spelling.</b> A legal SQL Server column 796 * can be written {@code [[foo]]]}, whose stored name is {@code [foo]} — 797 * delimiter-shaped. Publishing {@code [foo]} hands the next consumer of the 798 * name a string it decodes AGAIN, to {@code foo}, and source and target 799 * stop matching. Verified: that spelling loses its {@code fdd} edge 800 * outright.</li> 801 * </ol> 802 * 803 * <p>In both cases the caller keeps the ORIGINAL spelling. A consumer then 804 * still has to decode the name itself, which is worse than a clean name but 805 * far better than a dropped dependency — a missing edge is invisible in the 806 * output, so nobody can tell it is missing. 807 */ 808 private static boolean publishable(EDbVendor vendor, String decoded) { 809 if (decoded.indexOf('.') != -1) { 810 return false; 811 } 812 return decoded.equals(SQLUtil.decodeDelimitedName(vendor, ESQLDataObjectType.dotColumn, decoded)); 813 } 814 815 /** 816 * Probe cache for {@link #delimitersAreCosmetic}: one answer per vendor. 817 * {@link java.util.EnumMap} rather than a static table so the answer comes 818 * from the identifier façade and follows it if the vendor rules change. 819 */ 820 private static final java.util.Map<EDbVendor, Boolean> COSMETIC_DELIMITERS = 821 java.util.Collections.synchronizedMap(new java.util.EnumMap<EDbVendor, Boolean>(EDbVendor.class)); 822 823 /** 824 * Does quoting a COLUMN name change which column it names, for this vendor? 825 * 826 * <p>Asked by round-tripping a deliberately MIXED-CASE probe through the 827 * vendor's own canonical quoted spelling and then asking 828 * {@link IdentifierService#areEqualStatic} whether the quoted and bare forms 829 * are the same identifier. Mixed case is the discriminator: it is exactly 830 * what a case-folding vendor's delimiters protect. Equal ⇒ the delimiters 831 * are decoration and can be dropped from a published name; not equal ⇒ they 832 * are part of the identity and must stay. 833 * 834 * <p>Computed from the façade rather than listed per vendor, so it tracks 835 * the identifier rules instead of drifting from them. 836 */ 837 private static boolean delimitersAreCosmetic(EDbVendor vendor) { 838 Boolean cached = COSMETIC_DELIMITERS.get(vendor); 839 if (cached != null) { 840 return cached.booleanValue(); 841 } 842 boolean cosmetic; 843 try { 844 String probe = "GspDelimiterProbe"; 845 String quoted = SQLUtil.quoteDottedName(vendor, ESQLDataObjectType.dotColumn, probe); 846 cosmetic = !quoted.equals(probe) 847 && IdentifierService.areEqualStatic(vendor, ESQLDataObjectType.dotColumn, quoted, probe); 848 } catch (RuntimeException e) { 849 // A vendor the façade cannot answer for keeps its spellings as written. 850 cosmetic = false; 851 } 852 COSMETIC_DELIMITERS.put(vendor, Boolean.valueOf(cosmetic)); 853 return cosmetic; 854 } 855 856 /** 857 * Legacy display/normal-name form of a column reference (global analysis vendor). 858 * NOT an equality primitive: to test whether two names refer to the same column use 859 * {@link #sameName}, {@link #sameColumnSegment} or {@link #sameColumnName} — 860 * comparing normal names with {@code equals}/{@code equalsIgnoreCase} is the pre-P0d 861 * bug pattern. Model display-name bookkeeping is the one documented exception (see 862 * the identifier normalization guide §6). 863 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 864 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 865 * {@link LegacyDlineageWireNamesV1#columnName(String)}. 866 */ 867 @Deprecated 868 public static String getIdentifierNormalColumnName(String name) { 869 return LegacyDlineageWireNamesV1.columnName(name); 870 } 871 872 /** 873 * Legacy display/normal-name form — for equality use {@link #sameName} / 874 * {@link #sameColumnSegment} / {@link #sameColumnName}, not string compares on 875 * this result. 876 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 877 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 878 * {@link LegacyDlineageWireNamesV1#columnName(EDbVendor, String)}. 879 */ 880 @Deprecated 881 public static String getIdentifierNormalColumnName(String name, EDbVendor dbVendor) { 882 return LegacyDlineageWireNamesV1.columnName(dbVendor, name); 883 } 884 885 /** 886 * Legacy uppercase-collapsed normal form (global analysis vendor) — for equality use 887 * {@link #sameName} / {@link #sameColumnSegment}, not string compares on this result. 888 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 889 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 890 * {@link LegacyDlineageWireNamesV1#columnSegment(String)}. 891 */ 892 @Deprecated 893 public static String normalizeColumnName(String name) { 894 return LegacyDlineageWireNamesV1.columnSegment(name); 895 } 896 897 /** 898 * Legacy uppercase-collapsed normal form — for equality use {@link #sameName} / 899 * {@link #sameColumnSegment}, not string compares on this result. 900 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 901 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 902 * {@link LegacyDlineageWireNamesV1#columnSegment(EDbVendor, String)}. 903 */ 904 @Deprecated 905 public static String normalizeColumnName(String name, EDbVendor dbVendor) { 906 return LegacyDlineageWireNamesV1.columnSegment(dbVendor, name); 907 } 908 909 910 /** 911 * Legacy display/normal-name form of a table reference (global analysis vendor; 912 * dblink and call-shaped names keep their suffix). NOT an equality primitive — for 913 * equality use {@link #sameName} / {@link #sameQualifiedName} / 914 * {@link #compareTableIdentifier}. 915 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 916 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 917 * {@link LegacyDlineageWireNamesV1#tableName(String)}. 918 */ 919 @Deprecated 920 public static String getIdentifierNormalTableName(String name) { 921 return LegacyDlineageWireNamesV1.tableName(name); 922 } 923 924 /** 925 * Legacy display/normal-name form of a function reference — for equality use 926 * {@link #sameName} with {@code dotFunction}, not string compares on this result. 927 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 928 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 929 * {@link LegacyDlineageWireNamesV1#functionName(String)}. 930 */ 931 @Deprecated 932 public static String getIdentifierNormalFunctionName(String name) { 933 return LegacyDlineageWireNamesV1.functionName(name); 934 } 935 936 /** 937 * Legacy display/normal-name form (global analysis vendor) — for equality use 938 * {@link #sameName} / {@link #sameQualifiedName}, not string compares on this result. 939 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 940 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 941 * {@link LegacyDlineageWireNamesV1#name(String, ESQLDataObjectType)}. 942 */ 943 @Deprecated 944 public static String getIdentifierNormalName(String name, ESQLDataObjectType sqlDataObjectType) { 945 return LegacyDlineageWireNamesV1.name(name, sqlDataObjectType); 946 } 947 948 /** 949 * NORMALIZED display form of the column (last) segment of an AST object name. For 950 * equality prefer {@link #sameColumnName(TObjectName, TObjectName)} — it feeds the 951 * RAW segment to the canonical engine instead of comparing normalized display text. 952 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 953 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 954 * {@link LegacyDlineageWireNamesV1#columnName(TObjectName)}. 955 */ 956 @Deprecated 957 public static String getColumnName(TObjectName column) { 958 return LegacyDlineageWireNamesV1.columnName(column); 959 } 960 961 /** 962 * NORMALIZED display form of the column (last) segment of a model reference (naive 963 * last-dot split). For equality prefer {@link #sameColumnSegment(String, String)} — 964 * it feeds the RAW segment to the canonical engine instead of comparing normalized 965 * display text. 966 * @deprecated dlineage no longer calls it. For identity use the name service ({@link #sameName}, 967 * {@link #sameColumnName}, {@link #lexicalNameKey}); for a published name of output version 1, 968 * {@link LegacyDlineageWireNamesV1#columnName(String)} of {@link #columnSegmentOf}. 969 */ 970 @Deprecated 971 public static String getColumnName(String column) { 972 if (column == null) { 973 return null; 974 } 975 String name = column.substring(column.lastIndexOf(".") + 1); 976 if (name == null || "".equals(name.trim())) { 977 return LegacyDlineageWireNamesV1.columnName(column.toString().trim()); 978 } else 979 return LegacyDlineageWireNamesV1.columnName(name.trim()); 980 } 981 982 public static boolean isTempTable(Table tableModel, EDbVendor vendor) { 983 if (SubType.temp_table == tableModel.getSubType()) { 984 return true; 985 } 986 switch (vendor) { 987 case dbvmssql: 988 case dbvazuresql: 989 return tableModel.getName().startsWith("#") && !tableModel.getName().startsWith("##"); 990 default: 991 return false; 992 } 993 } 994 995 /** 996 * dlineage's display name of a table: qualified with the current default database and schema, 997 * in the form output version 1 publishes ({@link LegacyDlineageWireNamesV1#tableBucket}). Besides 998 * the display, it is only the compatibility bucket of a table that carries no identity. Not an 999 * identity, and not a name to look a table up with: see {@link #getTableIdentityKey} and 1000 * {@link #getTableFullNameAsWritten}. 1001 */ 1002 public static String getTableFullName(String tableName) { 1003 if (ModelBindingManager.get() != null && ModelBindingManager.get().isDblinkTable(tableName)) { 1004 return tableName; 1005 } 1006 if(tableName == null){ 1007 return null; 1008 } 1009 return LegacyDlineageWireNamesV1.tableBucket(getTableFullNameAsWritten(tableName)); 1010 } 1011 1012 /** Internal table binding key. Keep lexical provenance before legacy display folding. 1013 * BigQuery keys the fully qualified name; every other vendor keeps its legacy qualification 1014 * (the same default database/schema text {@link #getTableFullNameAsWritten} adds) and keys 1015 * that with the analysis's identifier rules instead of the vendor-default fold, so two tables 1016 * a configured profile keeps apart are not merged into one lineage model. 1017 */ 1018 public static Object getTableIdentityKey(String lexicalName) { 1019 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery) { 1020 if (lexicalName == null || lexicalName.isEmpty()) return null; 1021 // The analysis's own service — the same one the catalog and resolver used. Minting this 1022 // key from a freshly built vendor-default profile would key the lineage model under 1023 // rules the rest of the run never applied, so a configured analysis would dedup 1024 // tables differently from the way it resolved them. 1025 IdentifierService service = ModelBindingManager.getGlobalIdentifierService(); 1026 if (service == null) { 1027 service = IdentifierService.defaultServiceFor(EDbVendor.dbvbigquery); 1028 } 1029 IdentifierProfile profile = service.getProfile(); 1030 List<String> segments = new ArrayList<String>( 1031 QualifiedNameHierarchy.split(EDbVendor.dbvbigquery, lexicalName)); 1032 // Default catalog/schema values are stored names; source segments stay lexical. 1033 // Use the same active/default context as Table, without going through its display. 1034 if (segments.size() == 1) { 1035 String schema = ModelBindingManager.getGlobalSchema(); 1036 if (schema == null) schema = IdentifierCodec.encodeStored(profile, ESQLDataObjectType.dotSchema, 1037 ModelBindingManager.getEffectiveDefaultSchema()); 1038 segments.add(0, schema); 1039 } 1040 if (segments.size() == 2) { 1041 String database = ModelBindingManager.getGlobalDatabase(); 1042 if (database == null) database = IdentifierCodec.encodeStored(profile, ESQLDataObjectType.dotCatalog, 1043 ModelBindingManager.getEffectiveDefaultDatabase()); 1044 segments.add(0, database); 1045 } 1046 return service.qualifiedCanonKeyFromLexical( 1047 String.join(".", segments), ESQLDataObjectType.dotTable); 1048 } 1049 if (lexicalName == null) { 1050 return null; 1051 } 1052 String legacyKey = getTableFullName(lexicalName); 1053 IdentifierService service = analysisIdentifierService(); 1054 return service == null ? legacyKey 1055 : analysisTableKey(service, getTableFullNameAsWritten(lexicalName), legacyKey); 1056 } 1057 1058 public static Object getTableIdentityKeyWithDefaultSchema(String lexicalName) { 1059 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery) { 1060 return getTableIdentityKey(lexicalName); 1061 } 1062 if (lexicalName == null) { 1063 return null; 1064 } 1065 String legacyKey = getTableFullNameWithDefaultSchema(lexicalName); 1066 IdentifierService service = analysisIdentifierService(); 1067 return service == null ? legacyKey 1068 : analysisTableKey(service, getTableFullNameWithDefaultSchemaAsWritten(lexicalName), legacyKey); 1069 } 1070 1071 /** 1072 * Merge key of a physical table or view by the qualified name the output carries: the 1073 * analysis's identity where the name has one, else the legacy folded name. BigQuery tables 1074 * carry their own identity key and keep the folded name here. 1075 */ 1076 public static Object getQualifiedTableIdentityKey(String qualifiedName) { 1077 String legacyKey = LegacyDlineageWireNamesV1.tableBucket(qualifiedName); 1078 IdentifierService service = ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery 1079 ? null : configuredIdentifierService(); 1080 return service == null ? legacyKey : analysisTableKey(service, qualifiedName, legacyKey); 1081 } 1082 1083 /** 1084 * The merge key of a physical table or view in an output dataflow: its legacy folded name, 1085 * unless another table already holds that name with a different identity under the 1086 * analysis's rules, in which case it gets a key of its own. Keeping the legacy string 1087 * wherever the rules agree keeps merge order and column keys exactly as they were; only 1088 * tables the analysis keeps apart stop being merged into one. 1089 * 1090 * @param identitiesByLegacyName state shared by every table of one merge 1091 */ 1092 public static String physicalTableMergeKey(Map<String, List<Object>> identitiesByLegacyName, String legacyKey, 1093 String qualifiedTableName) { 1094 if (legacyKey == null || ModelBindingManager.getGlobalVendor() == EDbVendor.dbvbigquery) { 1095 return legacyKey; 1096 } 1097 IdentifierService service = configuredIdentifierService(); 1098 if (service == null) { 1099 return legacyKey; 1100 } 1101 Object identityKey = analysisTableKey(service, qualifiedTableName, legacyKey); 1102 if (!(identityKey instanceof TableIdentityKey)) { 1103 return legacyKey; 1104 } 1105 List<Object> identities = identitiesByLegacyName.get(legacyKey); 1106 if (identities == null) { 1107 identities = new ArrayList<Object>(); 1108 identitiesByLegacyName.put(legacyKey, identities); 1109 } 1110 int index = identities.indexOf(identityKey); 1111 if (index < 0) { 1112 identities.add(identityKey); 1113 index = identities.size() - 1; 1114 } 1115 return index == 0 ? legacyKey : legacyKey + "\u0001" + index; 1116 } 1117 1118 /** 1119 * The identity a published table name reads as, under the analysis's identifier rules: the name 1120 * taken as a SQL spelling. Weaker evidence than a carried identity, since a stored name is published 1121 * bare; {@link PhysicalTableMergeKeys} uses it only where no carried identity decides. 1122 * 1123 * @return null when the analysis has no service or the name has no identity (a database link, a 1124 * shape the vendor's hierarchy cannot split) 1125 */ 1126 public static QualifiedCanonKey publishedTableIdentity(String qualifiedTableName, String legacyKey) { 1127 IdentifierService service = analysisIdentifierService(); 1128 if (service == null || qualifiedTableName == null) { 1129 return null; 1130 } 1131 Object key = analysisTableKey(service, qualifiedTableName, legacyKey); 1132 return key instanceof TableIdentityKey ? ((TableIdentityKey) key).getCanonKey() : null; 1133 } 1134 1135 /** 1136 * The identifier rules a lineage table key minted from a SQL spelling follows: the analysis's 1137 * configured profile, else its vendor's defaults. Session defaults and {@code USE} names are SQL 1138 * spellings too (the catalog reads them that way: {@code setDefaultSchemaName("Mixed")} makes 1139 * {@code TSQLEnv.searchTable} miss {@code "Mixed".t}), so a name composed from them is one. 1140 */ 1141 private static IdentifierService analysisIdentifierService() { 1142 return ModelBindingManager.getGlobalIdentifierService(); 1143 } 1144 1145 private static final Map<IdentifierService, Boolean> CONFIGURED_RULES = 1146 java.util.Collections.synchronizedMap(new java.util.WeakHashMap<IdentifierService, Boolean>()); 1147 1148 /** 1149 * Whether the analysis runs under identifier rules of its own rather than its vendor's 1150 * defaults. Only then are lineage tables keyed by the analysis's identity: under the vendor 1151 * defaults they keep the legacy fold every existing output is built on. The two are not the 1152 * same relation for every vendor (Redshift delimited names, for one), and which of them is 1153 * right there is a question about the vendor rule, not one this key should answer silently. 1154 */ 1155 /** The analysis's identifier service when it configures rules of its own, else null. */ 1156 private static IdentifierService configuredIdentifierService() { 1157 IdentifierService service = ModelBindingManager.getGlobalIdentifierServiceRaw(); 1158 return service != null && configuresIdentifierRules(service) ? service : null; 1159 } 1160 1161 /** 1162 * The analysis's configured service when it applies to names of {@code vendor}: a configured profile is the 1163 * analysis's vendor's, so a metadata server of another vendor keeps that vendor's own rules. Null otherwise. 1164 */ 1165 private static IdentifierService configuredIdentifierService(EDbVendor vendor) { 1166 IdentifierService service = configuredIdentifierService(); 1167 return service != null && vendor != null && service.getProfile().getVendor() != vendor ? null : service; 1168 } 1169 1170 private static boolean configuresIdentifierRules(IdentifierService service) { 1171 Boolean configured = CONFIGURED_RULES.get(service); 1172 if (configured == null) { 1173 IdentifierProfile profile = service.getProfile(); 1174 configured = !profile.hasSameEffectivePolicyAs( 1175 IdentifierProfile.forVendor(profile.getVendor(), IdentifierProfile.VendorFlags.defaults())); 1176 CONFIGURED_RULES.put(service, configured); 1177 } 1178 return configured; 1179 } 1180 1181 /** 1182 * Keys a table name, qualified as written, under the analysis's identifier service. A name 1183 * the service has no identity for — a database link, or a shape the vendor's name hierarchy 1184 * cannot split — keeps its legacy folded key, which is what it was bound under before. 1185 */ 1186 private static Object analysisTableKey(IdentifierService service, String qualifiedAsWritten, String legacyKey) { 1187 if (qualifiedAsWritten == null || qualifiedAsWritten.isEmpty() 1188 || (ModelBindingManager.get() != null && ModelBindingManager.get().isDblinkTable(qualifiedAsWritten))) { 1189 return legacyKey; 1190 } 1191 EDbVendor vendor = service.getProfile().getVendor(); 1192 if (!QualifiedNameHierarchy.hasHierarchyMapping(vendor, ESQLDataObjectType.dotTable)) { 1193 return legacyKey; 1194 } 1195 // Only a path of valid identifiers has an identity. A name the hierarchy cannot plan, or 1196 // with a segment that is not an identifier at all (a database link written into the name, 1197 // a function-call shape), keeps the legacy key it was always bound under. 1198 List<QualifiedNameHierarchy.Segment> plan = QualifiedNameHierarchy.plan(vendor, ESQLDataObjectType.dotTable, 1199 QualifiedNameHierarchy.split(vendor, qualifiedAsWritten)); 1200 if (plan == null) { 1201 return legacyKey; 1202 } 1203 for (QualifiedNameHierarchy.Segment segment : plan) { 1204 if (!IdentifierCodec.isVendorValidSpelling(service.getProfile(), segment.getType(), segment.getText())) { 1205 return legacyKey; 1206 } 1207 } 1208 QualifiedCanonKey canonKey; 1209 try { 1210 canonKey = service.qualifiedCanonKeyFromLexical(qualifiedAsWritten, ESQLDataObjectType.dotTable); 1211 } catch (UnsupportedOperationException e) { 1212 return legacyKey; 1213 } catch (IllegalArgumentException e) { 1214 return legacyKey; 1215 } 1216 return canonKey == null ? legacyKey : new TableIdentityKey(canonKey, legacyKey); 1217 } 1218 1219 /** 1220 * The SQL spelling of a stored database or schema name: the bare name when the bare word, read as 1221 * SQL, is that stored name (PostgreSQL {@code public}), else its delimited spelling (a schema stored 1222 * as {@code Mixed} is {@code "Mixed"}). A name already delimited is returned as it is. 1223 */ 1224 public static String storedNameSpelling(ESQLDataObjectType role, String name) { 1225 if (name == null || name.isEmpty()) { 1226 return name; 1227 } 1228 IdentifierService service = analysisIdentifierService(); 1229 if (service == null) { 1230 service = IdentifierService.defaultServiceFor(ModelBindingManager.getGlobalVendor()); 1231 } 1232 IdentifierProfile profile = service.getProfile(); 1233 if (IdentifierCodec.isQuoted(profile, role, name)) { 1234 return name; 1235 } 1236 if (name.indexOf('.') < 0 && IdentifierCodec.isVendorValidSpelling(profile, role, name) 1237 && service.canonKeyOf(name, role, IdentifierInputForm.SQL_LEXICAL) 1238 .equals(service.canonKeyOf(name, role, IdentifierInputForm.CATALOG_STORED))) { 1239 return name; 1240 } 1241 return IdentifierCodec.hasQuotedForm(profile, role) ? IdentifierCodec.encodeStored(profile, role, name) : name; 1242 } 1243 1244 /** 1245 * The same qualification as {@link #getTableFullName}, with the name kept AS WRITTEN. Quote 1246 * state is part of a name's identity (Oracle {@code "Orders"} is not {@code Orders}, which 1247 * means {@code ORDERS}), so this is the form to ask the catalog with. 1248 */ 1249 public static String getTableFullNameAsWritten(String tableName) { 1250 if (ModelBindingManager.get() != null && ModelBindingManager.get().isDblinkTable(tableName)) { 1251 return tableName; 1252 } 1253 1254 if(tableName == null){ 1255 return null; 1256 } 1257 1258 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1259 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1260 tableName = SQLUtil.trimColumnStringQuote(tableName); 1261 } 1262 List<String> segments = SQLUtil.parseNames(tableName); 1263 if (segments.size() == 1) { 1264 StringBuffer buffer = new StringBuffer(); 1265 if (ModelBindingManager.getGlobalDatabaseSpelling() != null) { 1266 buffer.append(ModelBindingManager.getGlobalDatabaseSpelling()).append("."); 1267 } 1268 if (ModelBindingManager.getGlobalSchemaSpelling() != null) { 1269 buffer.append(ModelBindingManager.getGlobalSchemaSpelling()).append("."); 1270 } 1271 buffer.append(tableName); 1272 return (buffer.toString()); 1273 } else if (segments.size() == 2) { 1274 if (ModelBindingManager.getGlobalDatabaseSpelling() != null) { 1275 return (ModelBindingManager.getGlobalDatabaseSpelling() + "." + tableName); 1276 } else { 1277 return (tableName); 1278 } 1279 } else { 1280 if ((ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql 1281 || ModelBindingManager.getGlobalVendor() == EDbVendor.dbvazuresql) && tableName.indexOf("..") != -1) { 1282 if (ModelBindingManager.getGlobalSchemaSpelling() != null) { 1283 return (tableName.replace("..", "." + ModelBindingManager.getGlobalSchemaSpelling() + ".")); 1284 } else if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql 1285 || ModelBindingManager.getGlobalVendor() == EDbVendor.dbvazuresql) { 1286 return (tableName.replace("..", ".dbo.")); 1287 } else { 1288 return (tableName); 1289 } 1290 } else { 1291 return (tableName); 1292 } 1293 } 1294 } 1295 1296 1297 1298 public static String getSimpleTableName(String tableName) { 1299 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1300 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1301 tableName = SQLUtil.trimColumnStringQuote(tableName); 1302 } 1303 List<String> segments = SQLUtil.parseNames(tableName); 1304 return segments.get(segments.size()-1); 1305 } 1306 1307 public static String getColumnNameOnly(String columnName) { 1308 if (columnName.startsWith("`") && columnName.endsWith("`") && columnName.indexOf(".") != -1 1309 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1310 columnName = SQLUtil.trimColumnStringQuote(columnName); 1311 } 1312 List<String> segments = SQLUtil.parseNames(columnName); 1313 return segments.get(segments.size()-1); 1314 } 1315 1316 public static String getTableSchema(String tableName) { 1317 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1318 boolean supportSchema = TSQLEnv.supportSchema(vendor); 1319 1320 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1321 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1322 tableName = SQLUtil.trimColumnStringQuote(tableName); 1323 } 1324 List<String> segments = SQLUtil.parseNames(tableName); 1325 if (segments.size() == 1) { 1326 if (supportSchema && ModelBindingManager.getGlobalSchema() != null) { 1327 return ModelBindingManager.getGlobalSchema(); 1328 } 1329 } else if (segments.size() == 2) { 1330 if(supportSchema) { 1331 return segments.get(0); 1332 } 1333 } else { 1334 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql && tableName.indexOf("..") != -1) { 1335 if (ModelBindingManager.getGlobalSchema() != null) { 1336 return ModelBindingManager.getGlobalSchema(); 1337 } else if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) { 1338 return "dbo"; 1339 } 1340 } else { 1341 if(supportSchema) { 1342 return segments.get(segments.size() - 2); 1343 } 1344 } 1345 } 1346 return null; 1347 } 1348 1349 public static String getTableServer(String tableName) { 1350 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1351 boolean supportCatalog = TSQLEnv.supportCatalog(vendor); 1352 boolean supportSchema = TSQLEnv.supportSchema(vendor); 1353 1354 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1355 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1356 tableName = SQLUtil.trimColumnStringQuote(tableName); 1357 } 1358 List<String> segments = SQLUtil.parseNames(tableName); 1359 if (segments.size() <= 2) { 1360 return ModelBindingManager.getGlobalServer(); 1361 } else if (segments.size() == 3) { 1362 if(supportCatalog && supportSchema) { 1363 return ModelBindingManager.getGlobalServer(); 1364 } else { 1365 return segments.get(0); 1366 } 1367 } else { 1368 if(supportCatalog && supportSchema) { 1369 return segments.get(segments.size() - 4); 1370 } else { 1371 return segments.get(segments.size() - 3); 1372 } 1373 } 1374 } 1375 1376 public static String getTableDatabase(String tableName) { 1377 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1378 boolean supportCatalog = TSQLEnv.supportCatalog(vendor); 1379 boolean supportSchema = TSQLEnv.supportSchema(vendor); 1380 1381 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1382 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1383 tableName = SQLUtil.trimColumnStringQuote(tableName); 1384 } 1385 List<String> segments = SQLUtil.parseNames(tableName); 1386 if (segments.size() == 1) { 1387 if (supportCatalog && ModelBindingManager.getGlobalDatabase() != null) { 1388 return ModelBindingManager.getGlobalDatabase(); 1389 } 1390 } else if (segments.size() == 2) { 1391 if(supportCatalog && supportSchema) { 1392 if (ModelBindingManager.getGlobalDatabase() != null) { 1393 return ModelBindingManager.getGlobalDatabase(); 1394 } 1395 } else { 1396 if (supportCatalog) { 1397 return segments.get(0); 1398 } 1399 } 1400 } else { 1401 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql && tableName.indexOf("..") != -1) { 1402 String fullName = tableName; 1403 if (ModelBindingManager.getGlobalSchema() != null) { 1404 fullName = LegacyDlineageWireNamesV1.tableName( 1405 tableName.replace("..", "." + ModelBindingManager.getGlobalSchema() + ".")); 1406 } else if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) { 1407 fullName = LegacyDlineageWireNamesV1.tableName(tableName.replace("..", ".dbo.")); 1408 } else { 1409 fullName = LegacyDlineageWireNamesV1.tableName(tableName); 1410 } 1411 return getTableDatabase(fullName); 1412 } else { 1413 if(supportCatalog && supportSchema) { 1414 return segments.get(segments.size() - 3); 1415 } 1416 else if(supportCatalog){ 1417 return segments.get(segments.size() - 2); 1418 } 1419 } 1420 } 1421 return null; 1422 } 1423 1424 public static String getTableFullNameWithDefaultSchema(String tableName) { 1425 return LegacyDlineageWireNamesV1.tableBucket(getTableFullNameWithDefaultSchemaAsWritten(tableName)); 1426 } 1427 1428 /** 1429 * {@link #getTableFullNameWithDefaultSchema} before the legacy fold: the default database and 1430 * schema added as text, the name itself left as written. 1431 */ 1432 public static String getTableFullNameWithDefaultSchemaAsWritten(String tableName) { 1433 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1434 boolean supportCatalog = TSQLEnv.supportCatalog(vendor); 1435 boolean supportSchema = TSQLEnv.supportSchema(vendor); 1436 1437 if (tableName.startsWith("`") && tableName.endsWith("`") && tableName.indexOf(".") != -1 1438 && ModelBindingManager.getGlobalOption().getVendor() == EDbVendor.dbvbigquery) { 1439 tableName = SQLUtil.trimColumnStringQuote(tableName); 1440 } 1441 List<String> segments = SQLUtil.parseNames(tableName); 1442 if (segments.size() == 1) { 1443 StringBuffer buffer = new StringBuffer(); 1444 if (supportCatalog && ModelBindingManager.getGlobalDatabase() != null) { 1445 buffer.append(ModelBindingManager.getGlobalDatabase()).append("."); 1446 } 1447 if (supportSchema && ModelBindingManager.getGlobalSchema() != null) { 1448 buffer.append(ModelBindingManager.getGlobalSchema()).append("."); 1449 } else if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) { 1450 buffer.append("dbo").append("."); 1451 } 1452 buffer.append(tableName); 1453 return (buffer.toString()); 1454 } else if (segments.size() == 2) { 1455 if (supportCatalog && supportSchema && ModelBindingManager.getGlobalDatabase() != null) { 1456 return (ModelBindingManager.getGlobalDatabase() + "." + tableName); 1457 } else { 1458 return (tableName); 1459 } 1460 } else { 1461 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql && tableName.indexOf("..") != -1) { 1462 if (ModelBindingManager.getGlobalSchema() != null) { 1463 return ( 1464 tableName.replace("..", "." + ModelBindingManager.getGlobalSchema() + ".")); 1465 } else if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) { 1466 return (tableName.replace("..", ".dbo.")); 1467 } else { 1468 return (tableName); 1469 } 1470 } else { 1471 return (tableName); 1472 } 1473 } 1474 } 1475 1476 @SuppressWarnings("rawtypes") 1477 public static SqlInfo[] convertSQL(File file, String json) { 1478 List<SqlInfo> sqlInfos = new ArrayList<SqlInfo>(); 1479 try { 1480 List sqlContents = (List) JSON.parseObject(json); 1481 for (int j = 0; j < sqlContents.size(); j++) { 1482 Map sqlContent = (Map) sqlContents.get(j); 1483 String sql = (String) sqlContent.get("sql"); 1484 String fileName = (String) sqlContent.get("fileName"); 1485 String filePath = (String) sqlContent.get("filePath"); 1486 if (sql != null && sql.trim().startsWith("{")) { 1487 Map queryObject = (Map) JSON.parseObject(sql); 1488 List querys = (List) queryObject.get("queries"); 1489 if (querys != null) { 1490 for (int i = 0; i < querys.size(); i++) { 1491 Map object = (Map) querys.get(i); 1492 SqlInfo info = new SqlInfo(); 1493 info.setSql(JSON.toJSONString(object)); 1494 info.setFileName(fileName); 1495 info.setFilePath(filePath); 1496 info.setOriginIndex(i); 1497 sqlInfos.add(info); 1498 } 1499 } else { 1500 SqlInfo info = new SqlInfo(); 1501 info.setSql(JSON.toJSONString(queryObject)); 1502 info.setFileName(fileName); 1503 info.setFilePath(filePath); 1504 info.setOriginIndex(0); 1505 sqlInfos.add(info); 1506 } 1507 } else if (sql != null) { 1508 SqlInfo info = new SqlInfo(); 1509 info.setSql(sql); 1510 info.setFileName(fileName); 1511 info.setFilePath(filePath); 1512 info.setOriginIndex(0); 1513 sqlInfos.add(info); 1514 } 1515 } 1516 } catch (Exception e) { 1517 try { 1518 Map queryObject = (Map) JSON.parseObject(json); 1519 List querys = (List) queryObject.get("queries"); 1520 if (querys != null) { 1521 for (int i = 0; i < querys.size(); i++) { 1522 Map object = (Map) querys.get(i); 1523 SqlInfo info = new SqlInfo(); 1524 info.setSql(JSON.toJSONString(object)); 1525 if (file != null) { 1526 info.setFileName(file.getName()); 1527 info.setFilePath(file.getAbsolutePath()); 1528 } 1529 info.setOriginIndex(i); 1530 sqlInfos.add(info); 1531 } 1532 } else { 1533 SqlInfo info = new SqlInfo(); 1534 info.setSql(JSON.toJSONString(queryObject)); 1535 if (file != null) { 1536 info.setFileName(file.getName()); 1537 info.setFilePath(file.getAbsolutePath()); 1538 } 1539 info.setOriginIndex(0); 1540 sqlInfos.add(info); 1541 } 1542 } catch (Exception e1) { 1543 SqlInfo info = new SqlInfo(); 1544 info.setSql(json); 1545 if (file != null) { 1546 info.setFileName(file.getName()); 1547 info.setFilePath(file.getAbsolutePath()); 1548 } 1549 info.setOriginIndex(0); 1550 sqlInfos.add(info); 1551 } 1552 } 1553 return sqlInfos.toArray(new SqlInfo[0]); 1554 } 1555 1556 1557 public static String getQualifiedTableName(table table) { 1558 boolean supportCatalog = TSQLEnv.supportCatalog(ModelBindingManager.getGlobalVendor()); 1559 boolean supportSchema = TSQLEnv.supportSchema(ModelBindingManager.getGlobalVendor()); 1560 StringBuilder buffer = new StringBuilder(); 1561 if (supportCatalog && supportSchema) { 1562 if (!SQLUtil.isEmpty(table.getDatabase())) { 1563 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvoracle 1564 && SubType.dblink.name().equals(table.getSubType())) { 1565 //dblink doesn't set database 1566 } else { 1567 buffer.append(table.getDatabase()); 1568 } 1569 } 1570 if (!SQLUtil.isEmpty(table.getSchema())) { 1571 if (buffer.length() > 0) { 1572 buffer.append("."); 1573 } 1574 buffer.append(table.getSchema()); 1575 } else { 1576 if (buffer.length() > 0) { 1577 buffer.append("."); 1578 if (ModelBindingManager.getGlobalVendor() == EDbVendor.dbvmssql) { 1579 buffer.append("dbo"); 1580 } else { 1581 buffer.append(TSQLEnv.DEFAULT_SCHEMA_NAME); 1582 } 1583 } 1584 } 1585 if (buffer.length() > 0) { 1586 buffer.append("."); 1587 } 1588 buffer.append(table.getTableNameOnly()); 1589 } 1590 else if(supportCatalog) { 1591 if (!SQLUtil.isEmpty(table.getDatabase())) { 1592 buffer.append(table.getDatabase()); 1593 } 1594 if (buffer.length() > 0) { 1595 buffer.append("."); 1596 } 1597 buffer.append(table.getTableNameOnly()); 1598 } 1599 else if(supportSchema) { 1600 if (!SQLUtil.isEmpty(table.getSchema())) { 1601 buffer.append(table.getSchema()); 1602 } 1603 if (buffer.length() > 0) { 1604 buffer.append("."); 1605 } 1606 buffer.append(table.getTableNameOnly()); 1607 } 1608 if (SubType.dblink.name().equals(table.getSubType())) { 1609 buffer.append("@").append(table.getDatabase()); 1610 } 1611 return buffer.toString(); 1612 } 1613 1614 public static TCustomSqlStatement getTopStmt(TCustomSqlStatement stmt) { 1615 if (ModelBindingManager.getGlobalOption() != null && ModelBindingManager.getGlobalOption().getAnalyzeMode() == AnalyzeMode.crud) { 1616 TCustomSqlStatement parent = stmt.getParentStmt(); 1617 if (parent == null || parent instanceof TPlsqlCreatePackage 1618 || parent instanceof TStoredProcedureSqlStatement || parent instanceof TCommonBlock || parent instanceof TMssqlBlock) 1619 return stmt; 1620 return getTopStmt(parent); 1621 } else { 1622 TCustomSqlStatement parent = stmt.getParentStmt(); 1623 if (parent == null || parent instanceof TPlsqlCreatePackage) 1624 return stmt; 1625 return getTopStmt(parent); 1626 } 1627 } 1628 1629 public static TCustomSqlStatement getTopBasicStmt(TCustomSqlStatement stmt) { 1630 TCustomSqlStatement parent = stmt.getParentStmt(); 1631 if (parent == null || parent instanceof TPlsqlCreatePackage || parent instanceof TStoredProcedureSqlStatement 1632 || parent instanceof TCommonBlock || parent instanceof TMssqlBlock) 1633 return stmt; 1634 return getTopBasicStmt(parent); 1635 } 1636 1637 public static boolean isQuote(String column) { 1638 return (column.startsWith("\"") && column.endsWith("\"")) || (column.startsWith("'") && column.endsWith("'")) 1639 || (column.startsWith("[") && column.endsWith("]")) || (column.startsWith("`") && column.endsWith("`")); 1640 } 1641 1642 public static TObjectName getProcedureOrFunctionName(TStoredProcedureSqlStatement stmt) { 1643 if (stmt instanceof TCreateFunctionStmt) { 1644 return ((TCreateFunctionStmt) stmt).getFunctionName(); 1645 } 1646 if (stmt instanceof TMssqlCreateFunction) { 1647 return ((TMssqlCreateFunction) stmt).getFunctionName(); 1648 } 1649 return stmt.getStoredProcedureName(); 1650 } 1651 1652 public static String getProcedureNameWithArgs(TStoredProcedureSqlStatement stmt) { 1653 StringBuilder buffer = new StringBuilder(); 1654 TObjectName name = getProcedureOrFunctionName(stmt); 1655 if (name == null) { 1656 return null; 1657 } 1658 buffer.append(name.toString()); 1659 if (stmt.getParameterDeclarations() != null && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1660 buffer.append("("); 1661 TParameterDeclarationList parameters = stmt.getParameterDeclarations(); 1662 for (int i = 0; i < parameters.size(); ++i) { 1663 TParameterDeclaration parameter = parameters.getParameterDeclarationItem(i); 1664 if(parameter.getDataType()!=null) { 1665 buffer.append(parameter.getDataType().getDataTypeName()); 1666 } 1667 if(i<parameters.size()-1) { 1668 buffer.append(","); 1669 } 1670 } 1671 buffer.append(")"); 1672 } 1673 return buffer.toString(); 1674 } 1675 1676 public static String getIdentifierProcedureNameWithArgNum(procedure procedure) { 1677 StringBuilder buffer = new StringBuilder(); 1678 buffer.append(SQLUtil.trimColumnStringQuote(procedure.getName())); 1679 if (procedure.getArguments() != null && procedure.getArguments().size() > 0 && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1680 buffer.append("("); 1681 buffer.append(procedure.getArguments().size()); 1682 buffer.append(")"); 1683 } 1684 return buffer.toString().toUpperCase(java.util.Locale.ROOT); 1685 } 1686 1687 public static String getIdentifierOraclePackageNameWithArgNum(oraclePackage pkg) { 1688 StringBuilder buffer = new StringBuilder(); 1689 buffer.append(SQLUtil.trimColumnStringQuote(pkg.getName())); 1690 if (pkg.getArguments() != null && pkg.getArguments().size() > 0 && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1691 buffer.append("("); 1692 buffer.append(pkg.getArguments().size()); 1693 buffer.append(")"); 1694 } 1695 return buffer.toString().toUpperCase(java.util.Locale.ROOT); 1696 } 1697 1698 public static String getIdentifierFunctionName(table function) { 1699 StringBuilder buffer = new StringBuilder(); 1700 buffer.append(SQLUtil.trimColumnStringQuote(function.getName())); 1701 return buffer.toString().toUpperCase(java.util.Locale.ROOT); 1702 } 1703 1704 public static String getProcedureNameWithArgNum(TStoredProcedureSqlStatement stmt) { 1705 StringBuilder buffer = new StringBuilder(); 1706 TObjectName name = getProcedureOrFunctionName(stmt); 1707 if (name == null) { 1708 return null; 1709 } 1710 buffer.append(name.toString()); 1711 if (stmt.getParameterDeclarations() != null && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1712 buffer.append("("); 1713 buffer.append(stmt.getParameterDeclarations().size()); 1714 buffer.append(")"); 1715 } 1716 return buffer.toString(); 1717 } 1718 1719 public static String getProcedureNameWithInputArgNum(TStoredProcedureSqlStatement stmt) { 1720 StringBuilder buffer = new StringBuilder(); 1721 TObjectName name = getProcedureOrFunctionName(stmt); 1722 if (name == null) { 1723 return null; 1724 } 1725 buffer.append(name.toString()); 1726 if (stmt.getParameterDeclarations() != null && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1727 buffer.append("("); 1728 int count = 0; 1729 for (int i = 0; i < stmt.getParameterDeclarations().size(); i++) { 1730 if (stmt.getParameterDeclarations().getParameterDeclarationItem(i) 1731 .getParameterMode() == EParameterMode.in) { 1732 count++; 1733 } 1734 } 1735 if (count == 0) { 1736 count = stmt.getParameterDeclarations().size(); 1737 } 1738 buffer.append(count); 1739 buffer.append(")"); 1740 } 1741 return buffer.toString(); 1742 } 1743 1744 public static String getFunctionNameWithArgNum(TFunctionCall function) { 1745 StringBuilder buffer = new StringBuilder(); 1746 buffer.append(function.getFunctionName().toString()); 1747 if (function.getArgs() != null && DlineageUtil.supportFunctionOverride(ModelBindingManager.getGlobalVendor())) { 1748 buffer.append("("); 1749 buffer.append(function.getArgs().size()); 1750 buffer.append(")"); 1751 } 1752 return buffer.toString(); 1753 } 1754 1755 public static String getTableSchema(Table tableModel) { 1756 StringBuilder buffer = new StringBuilder(); 1757 if(tableModel.getDatabase()!=null) { 1758 buffer.append(tableModel.getDatabase()); 1759 } 1760 else { 1761 buffer.append(TSQLEnv.DEFAULT_DB_NAME); 1762 } 1763 buffer.append("."); 1764 if(tableModel.getSchema()!=null) { 1765 buffer.append(tableModel.getSchema()); 1766 } 1767 else { 1768 buffer.append(TSQLEnv.DEFAULT_SCHEMA_NAME); 1769 } 1770 return buffer.toString(); 1771 } 1772 1773 public static String getTableSQLEnvKey(Table tableModel) { 1774 StringBuilder buffer = new StringBuilder(); 1775 if(tableModel.getDatabase()!=null) { 1776 buffer.append(tableModel.getDatabase()); 1777 } 1778 else { 1779 buffer.append(TSQLEnv.DEFAULT_DB_NAME); 1780 } 1781 buffer.append("."); 1782 if(tableModel.getSchema()!=null) { 1783 buffer.append(tableModel.getSchema()); 1784 } 1785 else { 1786 buffer.append(TSQLEnv.DEFAULT_SCHEMA_NAME); 1787 } 1788 buffer.append("."); 1789 buffer.append(DlineageUtil.getSimpleTableName(tableModel.getName())); 1790 return buffer.toString(); 1791 } 1792 1793 /** 1794 * B1 (routine-summary-scc-design §3): overload-discriminating scope key. 1795 * The legacy scope key deliberately collides same-named overloads 1796 * (long-shipped LEGACY behavior); appending a stable digest of the 1797 * canonical signature descriptor yields one distinct key per overload for 1798 * the identity-first lookups of SHADOW/ENHANCED (slice B3). Run-scoped 1799 * only — the digest is Java hashCode-based and must never be persisted. 1800 */ 1801 public static String identityScopeKey(String legacyScopeKey, 1802 gudusoft.gsqlparser.dlineage.dynamicsql.RoutineIdentity identity) { 1803 if (legacyScopeKey == null || identity == null) { 1804 return null; 1805 } 1806 // Normalize the legacy part FIRST (the same getTableFullName pass the 1807 // legacy registration key goes through), then append the discriminator 1808 // outside normalization so '#' is never fed to identifier folding. 1809 // The discriminator is the INJECTIVE canonical signature — a hash 1810 // digest collided across valid Oracle overloads and merged their 1811 // pools (codex-B3-r3 finding 2b). 1812 // The \u0002 prefix marks the key (and every key derived from it by 1813 // suffixing ".var") as IDENTITY-SCOPED: lookups must never 1814 // re-interpret such a key against another active scope 1815 // (codex-B3-r4 finding 1). No legacy registration key starts with a 1816 // control character. 1817 return "\u0002" + legacyScopeKey + "#" + identity.signatureKey(); 1818 } 1819 1820 public static String getProcedureParentName(TCustomSqlStatement stmt) { 1821 if (stmt instanceof TStoredProcedureSqlStatement) { 1822 if (((TStoredProcedureSqlStatement) stmt).getStoredProcedureName() != null) { 1823 return getOraclePackageName() + ((TStoredProcedureSqlStatement) stmt).getStoredProcedureName().toString(); 1824 } 1825 } 1826 stmt = stmt.getParentStmt(); 1827 if (stmt == null) 1828 return null; 1829 1830 if (stmt instanceof TCommonBlock) { 1831 if(((TCommonBlock) stmt).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) { 1832 stmt = (TStoredProcedureSqlStatement)((TCommonBlock) stmt).getBlockBody().getParentObjectName(); 1833 } 1834 } 1835 1836 if (stmt instanceof TStoredProcedureSqlStatement) { 1837 if (((TStoredProcedureSqlStatement) stmt).getStoredProcedureName() != null) { 1838 String procedureName = getOraclePackageName() + ((TStoredProcedureSqlStatement) stmt).getStoredProcedureName().toString(); 1839 return getTableFullName(procedureName); 1840 } 1841 } 1842 if (stmt instanceof TTeradataCreateProcedure) { 1843 if (((TTeradataCreateProcedure) stmt).getProcedureName() != null) { 1844 String procedureName = ((TTeradataCreateProcedure) stmt).getProcedureName().toString(); 1845 return getTableFullName(procedureName); 1846 } 1847 } 1848 1849 return getProcedureParentName(stmt); 1850 } 1851 1852 private static String getOraclePackageName() { 1853 if (ModelBindingManager.getGlobalOraclePackage() != null) { 1854 return ModelBindingManager.getGlobalOraclePackage().getName() + "."; 1855 } 1856 return ""; 1857 } 1858 1859 public static sourceColumn copySourceColumn(sourceColumn oldSourceName) { 1860 sourceColumn newSourceColumn = new sourceColumn(); 1861 newSourceColumn.setClauseType(oldSourceName.getClauseType()); 1862 newSourceColumn.setColumn(oldSourceName.getColumn()); 1863 newSourceColumn.setColumn_type(oldSourceName.getColumn_type()); 1864 newSourceColumn.setCoordinate(oldSourceName.getCoordinate()); 1865 newSourceColumn.setId(oldSourceName.getId()); 1866 newSourceColumn.setName(oldSourceName.getName()); 1867 newSourceColumn.setParent_alias(oldSourceName.getParent_alias()); 1868 newSourceColumn.setParent_id(oldSourceName.getParent_id()); 1869 newSourceColumn.setParent_name(oldSourceName.getParent_name()); 1870 newSourceColumn.setSource(oldSourceName.getSource()); 1871 newSourceColumn.setSource_id(oldSourceName.getSource_id()); 1872 newSourceColumn.setSource_name(oldSourceName.getSource_name()); 1873 newSourceColumn.setStruct(oldSourceName.isStruct()); 1874 newSourceColumn.setType(oldSourceName.getType()); 1875 newSourceColumn.setValue(oldSourceName.getValue()); 1876 if(oldSourceName.getTransforms()!=null) { 1877 newSourceColumn.setTransforms(new LinkedHashSet<transform>()); 1878 for(transform transform: oldSourceName.getTransforms()) { 1879 newSourceColumn.getTransforms().add(transform); 1880 } 1881 } 1882 if(oldSourceName.getCandidateParents()!=null) { 1883 newSourceColumn.setCandidateParents(new LinkedHashSet<candidateTable>()); 1884 for(candidateTable candidateTable: oldSourceName.getCandidateParents()) { 1885 newSourceColumn.getCandidateParents().add(candidateTable); 1886 } 1887 } 1888 return newSourceColumn; 1889 } 1890 1891 public static boolean supportFunctionOverride(EDbVendor vendor) { 1892 switch (vendor) { 1893 case dbvpostgresql: 1894 case dbvoracle: 1895 case dbvdb2: 1896 case dbvmysql: 1897 case dbvsnowflake: 1898 case dbvhana: 1899 case dbvgreenplum: 1900 case dbvgaussdb: 1901 case dbvedb: 1902 case dbvdameng: 1903 return true; 1904 default: 1905 return false; 1906 } 1907 } 1908 1909 private static final List<String> TERADATA_BUILTIN_FUNCTIONS = Arrays 1910 .asList(new String[] { "ACCOUNT", "CURRENT_DATE", "CURRENT_ROLE", "CURRENT_TIME", "CURRENT_TIMESTAMP", 1911 "CURRENT_USER", "DATABASE", "DATE", "PROFILE", "ROLE", "SESSION", "TIME", "USER", "SYSDATE", }); 1912 1913 public static boolean isBuiltInFunctionName(String functionName) { 1914 if (functionName == null) 1915 return false; 1916 try { 1917 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1918 if (vendor == EDbVendor.dbvteradata) { 1919 boolean result = TERADATA_BUILTIN_FUNCTIONS.contains(functionName.toUpperCase(java.util.Locale.ROOT)); 1920 if (result) { 1921 return true; 1922 } 1923 } 1924 1925 List<String> versions = functionChecker.getAvailableDbVersions(vendor); 1926 if (versions != null && versions.size() > 0) { 1927 for (int i = 0; i < versions.size(); i++) { 1928 // no caller-side folding: isBuiltInFunction folds with 1929 // Locale.ROOT itself; a default-locale toUpperCase here 1930 // breaks the lookup on Turkish JVMs 1931 boolean result = functionChecker.isBuiltInFunction(functionName, 1932 vendor, versions.get(i)); 1933 if (result) { 1934 return result; 1935 } 1936 } 1937 1938 // boolean result = 1939 // TERADATA_BUILTIN_FUNCTIONS.contains(object.toString()); 1940 // if (result) { 1941 // return true; 1942 // } 1943 } 1944 } catch (Exception e) { 1945 } 1946 1947 return false; 1948 } 1949 1950 public static boolean isKeyword(String objectName) { 1951 if (objectName == null) 1952 return false; 1953 try { 1954 EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor(); 1955 1956 List<String> versions = keywordChecker.getAvailableDbVersions(vendor); 1957 if (versions != null && versions.size() > 0) { 1958 for (int i = 0; i < versions.size(); i++) { 1959 List<String> segments = SQLUtil.parseNames(objectName); 1960 boolean result = keywordChecker.isKeyword(segments.get(segments.size() - 1), 1961 vendor, versions.get(i), false); 1962 if (result) { 1963 return result; 1964 } 1965 } 1966 } 1967 } catch (Exception e) { 1968 } 1969 1970 return false; 1971 } 1972 1973 public static TSelectSqlStatement getLeftStmt(TSelectSqlStatement stmt) { 1974 TSelectSqlStatement current = stmt.getLeftStmt(); 1975 if (current == null) return null; 1976 while (current.getLeftStmt() != null) { 1977 current = current.getLeftStmt(); 1978 } 1979 return current; 1980 } 1981 1982 public static String stripQuotesFromQualifiedName(String qualifiedName) { 1983 if (SQLUtil.isEmpty(qualifiedName)) { 1984 return qualifiedName; 1985 } 1986 1987 List<String> segments = SQLUtil.parseNames(qualifiedName); 1988 StringBuilder sb = new StringBuilder(); 1989 for (int i = 0; i < segments.size(); i++) { 1990 if (i > 0) { 1991 sb.append('.'); 1992 } 1993 String seg = segments.get(i); 1994 sb.append(TBaseType.getTextWithoutQuoted(seg)); 1995 } 1996 return sb.toString(); 1997 } 1998 1999 public static boolean isProcedureExcluded(String fullName) { 2000 if (SQLUtil.isEmpty(fullName)) { 2001 return false; 2002 } 2003 2004 Option option = ModelBindingManager.getGlobalOption(); 2005 if (option == null) { 2006 return false; 2007 } 2008 2009 Set<String> excludedNames = option.getExcludedProcedureNames(); 2010 Set<String> excludedPatterns = option.getExcludedProcedurePatterns(); 2011 2012 if ((excludedNames == null || excludedNames.isEmpty()) && 2013 (excludedPatterns == null || excludedPatterns.isEmpty())) { 2014 return false; 2015 } 2016 2017 if (excludedNames != null) { 2018 for (String excludedName : excludedNames) { 2019 if (DlineageUtil.compareIdentifier(fullName, excludedName, ESQLDataObjectType.dotFunction)) { 2020 return true; 2021 } 2022 } 2023 } 2024 2025 if (excludedPatterns != null && !excludedPatterns.isEmpty()) { 2026 // Generate the qualified-name and all of its trailing segment-suffixes so a 2027 // schema-qualified pattern can match a more-qualified name. e.g. the pattern 2028 // "SCHEM.*" matches the Snowflake procedure "DB.SCHEM.PROC" through the suffix 2029 // "SCHEM.PROC", and "PUBLIC.*" matches "COVID19.PUBLIC.INSERT_CDC_DATA". 2030 // Segments are parsed with quote-awareness so dots inside a quoted identifier 2031 // (e.g. "SCHEMA"."PROC.WITH.DOTS") are not split. 2032 List<String> candidates = buildQualifiedNameSuffixes(fullName); 2033 for (String pattern : excludedPatterns) { 2034 if (SQLUtil.isEmpty(pattern)) { 2035 continue; 2036 } 2037 String normalizedPattern = stripQuotesFromQualifiedName(pattern); 2038 for (String candidate : candidates) { 2039 if (globMatches(normalizedPattern, candidate)) { 2040 return true; 2041 } 2042 } 2043 } 2044 } 2045 2046 return false; 2047 } 2048 2049 /** 2050 * Whether {@code text} as a whole matches the exclusion pattern {@code glob}: {@code *} stands for any run of 2051 * characters and {@code ?} for one character, neither of them a line terminator, and every other character for 2052 * itself, an ASCII letter in either case. That is the result of the regex {@code (?i)^...$} the pattern used to be 2053 * turned into, compiled again for every name, computed here in one pass over the pattern: with several {@code *} 2054 * that regex could retry every way of splitting a long name between them. 2055 */ 2056 static boolean globMatches(String glob, String text) { 2057 int[] pattern = glob.codePoints().toArray(); 2058 int[] name = text.codePoints().toArray(); 2059 // matched[j]: the pattern read so far matches the first j code points of the name 2060 boolean[] matched = new boolean[name.length + 1]; 2061 boolean[] next = new boolean[name.length + 1]; 2062 matched[0] = true; 2063 for (int c : pattern) { 2064 next[0] = c == '*' && matched[0]; 2065 for (int j = 1; j <= name.length; j++) { 2066 int x = name[j - 1]; 2067 if (c == '*') { 2068 next[j] = matched[j] || (next[j - 1] && !isLineTerminator(x)); 2069 } else if (c == '?') { 2070 next[j] = matched[j - 1] && !isLineTerminator(x); 2071 } else { 2072 next[j] = matched[j - 1] && sameIgnoringAsciiCase(c, x); 2073 } 2074 } 2075 boolean[] swap = matched; 2076 matched = next; 2077 next = swap; 2078 } 2079 return matched[name.length]; 2080 } 2081 2082 /** The characters a regex {@code .} does not match without {@code DOTALL}. */ 2083 private static boolean isLineTerminator(int c) { 2084 return c == '\n' || c == '\r' || c == 0x85 || c == 0x2028 || c == 0x2029; 2085 } 2086 2087 /** {@code (?i)} without {@code UNICODE_CASE}: only an ASCII letter matches its other case. */ 2088 private static boolean sameIgnoringAsciiCase(int patternChar, int c) { 2089 if (patternChar == c) { 2090 return true; 2091 } 2092 if (patternChar >= 'a' && patternChar <= 'z') { 2093 return c == patternChar - 32; 2094 } 2095 if (patternChar >= 'A' && patternChar <= 'Z') { 2096 return c == patternChar + 32; 2097 } 2098 return false; 2099 } 2100 2101 /** 2102 * Builds the quote-stripped qualified name and each of its trailing segment-suffixes, 2103 * longest first. For {@code "DB"."SCHEM"."PROC"} this yields 2104 * {@code [DB.SCHEM.PROC, SCHEM.PROC, PROC]}. Used by procedure-exclusion pattern 2105 * matching so a schema-level pattern matches a database-qualified name. 2106 */ 2107 private static List<String> buildQualifiedNameSuffixes(String fullName) { 2108 List<String> suffixes = new ArrayList<String>(); 2109 List<String> segments = SQLUtil.parseNames(fullName); 2110 if (segments == null || segments.isEmpty()) { 2111 suffixes.add(stripQuotesFromQualifiedName(fullName)); 2112 return suffixes; 2113 } 2114 List<String> stripped = new ArrayList<String>(segments.size()); 2115 for (String seg : segments) { 2116 stripped.add(TBaseType.getTextWithoutQuoted(seg)); 2117 } 2118 for (int i = 0; i < stripped.size(); i++) { 2119 StringBuilder sb = new StringBuilder(); 2120 for (int j = i; j < stripped.size(); j++) { 2121 if (j > i) { 2122 sb.append('.'); 2123 } 2124 sb.append(stripped.get(j)); 2125 } 2126 suffixes.add(sb.toString()); 2127 } 2128 return suffixes; 2129 } 2130}