001package gudusoft.gsqlparser.util; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TGSqlParser; 005import gudusoft.gsqlparser.dlineage.dataflow.model.ModelBindingManager; 006import gudusoft.gsqlparser.dlineage.dataflow.model.SubType; 007import gudusoft.gsqlparser.dlineage.dataflow.model.xml.table; 008import gudusoft.gsqlparser.pp.para.GFmtOpt; 009import gudusoft.gsqlparser.pp.para.GFmtOptFactory; 010import gudusoft.gsqlparser.pp.para.styleenums.TCaseOption; 011import gudusoft.gsqlparser.pp.stmtformatter.FormatterFactory; 012import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 013import gudusoft.gsqlparser.sqlenv.IdentifierCodec; 014import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 015import gudusoft.gsqlparser.sqlenv.IdentifierService; 016import gudusoft.gsqlparser.sqlenv.TSQLEnv; 017import gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics; 018 019import java.io.*; 020import java.nio.charset.Charset; 021import java.security.MessageDigest; 022import java.util.*; 023import java.util.stream.Collectors; 024 025public class SQLUtil { 026 private static final Logger logger = LoggerFactory.getLogger(SQLUtil.class); 027 028 029 public static String formatSql( EDbVendor dbVendor, String inputQuery ) 030 { 031 String Result = inputQuery; 032 TGSqlParser sqlparser = new TGSqlParser( dbVendor ); 033 sqlparser.sqltext = inputQuery; 034 int ret = sqlparser.parse( ); 035 if ( ret == 0 ) 036 { 037 GFmtOpt option = GFmtOptFactory.newInstance(); 038 option.caseFuncname = TCaseOption.CoNoChange; 039 Result = FormatterFactory.pp(sqlparser, option); 040 } 041 return Result; 042 } 043 044 public static boolean isEmpty(String value) { 045 return value == null || value.trim().length() == 0; 046 } 047 048 public static String getFileContent(File file) { 049 String charset = null; 050 String sqlfilename = file.getAbsolutePath(); 051 int read = 0; 052 try { 053 FileInputStream fr = new FileInputStream(sqlfilename); 054 byte[] bom = new byte[4]; 055 fr.read(bom, 0, bom.length); 056 057 if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) 058 && (bom[3] == (byte) 0xFF)) { 059 charset = "UTF-32BE"; 060 read = 4; 061 } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) 062 && (bom[3] == (byte) 0x00)) { 063 charset = "UTF-32LE"; 064 read = 4; 065 } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { 066 charset = "UTF-8"; 067 read = 3; 068 } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { 069 charset = "UTF-16BE"; 070 read = 2; 071 } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { 072 charset = "UTF-16LE"; 073 read = 2; 074 } else { 075 charset = "UTF-8"; 076 read = 0; 077 } 078 079 fr.close(); 080 } catch (IOException e) { 081 logger.error("Check file encoding failed.", e); 082 } 083 084 Long filelength = file.length(); 085 byte[] filecontent = new byte[filelength.intValue()]; 086 try { 087 InputStream in = new BufferedInputStream(new FileInputStream(sqlfilename)); 088 in.read(filecontent); 089 in.close(); 090 } catch (IOException e) { 091 logger.error("read file content failed.", e); 092 } 093 094 byte[] content = new byte[filelength.intValue() - read]; 095 System.arraycopy(filecontent, read, content, 0, content.length); 096 097 try { 098 String fileContent = new String(content, charset == null ? Charset.defaultCharset().name() : charset); 099 return fileContent.replace((char) 160, (char) 32); 100 } catch (UnsupportedEncodingException e) { 101 logger.error("The OS does not support " + charset == null ? Charset.defaultCharset().name() : charset, e); 102 return null; 103 } 104 } 105 106 public static String getInputStreamContent(InputStream is, boolean close) { 107 try { 108 ByteArrayOutputStream out = new ByteArrayOutputStream(4096); 109 byte[] tmp = new byte[4096]; 110 while (true) { 111 int r = is.read(tmp); 112 if (r == -1) 113 break; 114 out.write(tmp, 0, r); 115 } 116 byte[] bytes = out.toByteArray(); 117 if (close) { 118 is.close(); 119 } 120 out.close(); 121 String content = new String(bytes); 122 return content; 123 } catch (IOException e) { 124 logger.error("read inputStream failed.", e); 125 } 126 return null; 127 } 128 129 public static String getFileContent(String filePath) { 130 if (filePath == null) 131 return ""; 132 File file = new File(filePath); 133 if (!file.exists() || file.isDirectory()) 134 return ""; 135 return getFileContent(file); 136 } 137 138 // Per-vendor default-flags profiles for quoteDottedName; profiles are 139 // immutable, so sharing one instance per vendor is safe. 140 private static final java.util.concurrent.ConcurrentHashMap<EDbVendor, IdentifierProfile> 141 DEFAULT_PROFILES = new java.util.concurrent.ConcurrentHashMap<EDbVendor, IdentifierProfile>(); 142 143 private static IdentifierProfile defaultProfile(EDbVendor vendor) { 144 IdentifierProfile profile = DEFAULT_PROFILES.get(vendor); 145 if (profile == null) { 146 profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 147 DEFAULT_PROFILES.putIfAbsent(vendor, profile); 148 } 149 return profile; 150 } 151 152 /** 153 * Quote one name segment so its dots survive qualified-name splitting 154 * ({@link #parseNames}), using the vendor's REAL quoted-identifier form. 155 * 156 * <p>This replaces the legacy 157 * {@code delimitedChar + trimColumnStringQuote(x) + delimitedChar} 158 * pattern, which used the OPEN delimiter on both ends and therefore 159 * produced malformed spellings for open≠close vendors — MSSQL's 160 * {@code [a.b[} — that only survived via lenient first/last-char 161 * stripping and broke {@link #parseNames} segmentation 162 * ({@code [a.b[.dbo} reads as ONE unterminated segment; the repaired 163 * {@code [a.b].dbo} reads as two). 164 * 165 * <p>Existing quoting on the input is removed by a REAL decode where 166 * possible: input the vendor's codec recognizes as quoted goes through 167 * {@link IdentifierCodec#decodeLexical} (so embedded escaped delimiters 168 * like {@code [a]]b.c]} decode to {@code a]b.c} and are not re-escaped 169 * twice); malformed or vendor-foreign wrappers fall back to the lenient 170 * legacy strip ({@link #trimColumnStringQuote}). The stored text is then 171 * re-encoded with {@link IdentifierCodec#encodeStored} (proper close 172 * delimiter and escaping). 173 * 174 * <p>A vendor with no quoted-identifier syntax at all (SOQL) gets the 175 * stored text back UNCHANGED — the vendor cannot express a quoted 176 * spelling, and manufacturing one here would plant strings the codec 177 * itself refuses to recognize (a trap for the U5 recognition swap). A 178 * dotted name for such a vendor is unrepresentable vendor-side; how it 179 * segments internally is not this helper's contract. 180 * 181 * @param vendor database vendor 182 * @param objectType syntactic role of the segment (catalog/schema/table/column) 183 * @param name the possibly-quoted, possibly-dotted name segment 184 * @return a quoted spelling whose interior dots are protected 185 */ 186 public static String quoteDottedName(EDbVendor vendor, ESQLDataObjectType objectType, String name) { 187 if (name == null) { 188 return null; 189 } 190 IdentifierProfile profile = defaultProfile(vendor); 191 String stored; 192 if ((vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) 193 && name.length() >= 2 && name.charAt(0) == '[' && name.charAt(name.length() - 1) == '[') { 194 // the legacy synthetic spelling this program eradicates ([a.b[, 195 // open delimiter on both ends, content raw and NEVER escaped — 196 // it may contain literal ]): its stored text is the raw content. 197 // Only the MSSQL/Azure producers ever emitted this shape; for 198 // any other vendor brackets are ordinary characters. 199 stored = name.substring(1, name.length() - 1); 200 } else if (IdentifierCodec.isQuoted(profile, objectType, name)) { 201 try { 202 stored = IdentifierCodec.decodeLexical(profile, objectType, name); 203 } catch (IllegalArgumentException e) { 204 // other malformed quoted spellings: lenient legacy strip 205 stored = trimColumnStringQuote(name); 206 } 207 } else { 208 // bare, or wrapped in a quote form this vendor does not use 209 // (metadata sources are sloppy): legacy strip handles both 210 stored = trimColumnStringQuote(name); 211 } 212 if (vendor == EDbVendor.dbvbigquery) { 213 // These strings are INTERNAL qualified-name keys in the 214 // parseNames domain, whose backtick grammar is doubling-based. 215 // BigQuery's VENDOR-lexical escape is backslash-style 216 // (IdentifierCodec), which parseNames must not adopt — the same 217 // bytes mean something else for MySQL backticks. Encode BigQuery 218 // internal keys with parseNames-domain doubling instead (for the 219 // dot-protection case this equals the legacy backtick wrap; 220 // BigQuery object IDs cannot contain backticks, so the escape 221 // divergence is unreachable in vendor-valid data). 222 return "`" + stored.replace("`", "``") + "`"; 223 } 224 if (IdentifierCodec.hasQuotedForm(profile, objectType)) { 225 return IdentifierCodec.encodeStored(profile, objectType, stored); 226 } 227 return stored; 228 } 229 230 /** 231 * Decode a delimited identifier spelling to the undelimited name a catalog 232 * stores, WITHOUT case folding: {@code [Volgorde]}, {@code "Volgorde"}, 233 * {@code `Volgorde`} and the SQL Server legacy column-alias form 234 * {@code 'Volgorde'} all decode to {@code Volgorde}, and escaped delimiters 235 * resolve ({@code [Esc]]aped]} → {@code Esc]aped}). 236 * 237 * <p>This is the decode half of {@link #normalizeIdentifier}, split out for 238 * callers that publish a DISPLAY name and must therefore preserve the 239 * author's letter case — dlineage's column names, whose consumers match 240 * them against {@code sys.columns} and other catalog assets. Recognition is 241 * vendor- and role-aware, so a form that is not a delimiter for the vendor 242 * is left alone (brackets are array subscripts, not delimiters, in 243 * BigQuery). 244 * 245 * <p>Input that the vendor's codec does not recognize as quoted, or that 246 * starts with a delimiter but is malformed ({@code [a.b[}), is returned 247 * UNCHANGED — guessing a payload by stripping the first and last code units 248 * is the fabrication this program removed. 249 * 250 * <p><b>The caller owns identifier provenance.</b> Only pass a string that 251 * came from an identifier token (a {@code TObjectName} part token or a 252 * {@code TAliasClause} alias name). A SQL string literal renders as quoted 253 * text too, and decoding one would silently corrupt the VALUE — for SQL 254 * Server, {@code 'literal value'} is a legal column alias spelling AND a 255 * legal string constant, and nothing in the string itself tells them apart. 256 * 257 * @param vendor database vendor 258 * @param objectType syntactic role of the name (quote rules are per-role: 259 * the SQL Server apostrophe form is recognized for 260 * {@code dotColumn} only) 261 * @param name a single, possibly-delimited name segment 262 * @return the decoded name, or {@code name} unchanged when it is not a 263 * well-formed delimited spelling for this vendor and role 264 */ 265 public static String decodeDelimitedName(EDbVendor vendor, ESQLDataObjectType objectType, String name) { 266 if (name == null || name.isEmpty()) { 267 return name; 268 } 269 IdentifierProfile profile = defaultProfile(vendor); 270 try { 271 return IdentifierCodec.decodeLexical(profile, objectType, name); 272 } catch (IllegalArgumentException e) { 273 return name; 274 } 275 } 276 277 public static String trimColumnStringQuote(String string) { 278 try { 279 if (string == null) 280 return string; 281 282 if (string.indexOf('.') != -1) { 283 List<String> splits = parseNames(string); 284 if (splits.size() > 1) { 285 StringBuilder buffer = new StringBuilder(); 286 for (int i = 0; i < splits.size(); i++) { 287 String segment = splits.get(i); 288 if (parseNames(trimColumnStringQuote(segment)).size() > 1) { 289 buffer.append(segment); 290 } else { 291 buffer.append(trimColumnStringQuote(segment)); 292 } 293 if (i < splits.size() - 1) { 294 buffer.append("."); 295 } 296 } 297 string = buffer.toString(); 298 return string; 299 } 300 } 301 if (string.length() < 2) { 302 return string; 303 } 304 if (string.startsWith("'") && string.endsWith("'")) 305 return string.substring(1, string.length() - 1); 306 else if (string.startsWith("\"") && string.endsWith("\"")) 307 return string.substring(1, string.length() - 1); 308 else if (string.startsWith("`") && string.endsWith("`")) 309 return string.substring(1, string.length() - 1); 310 else if (string.startsWith("[") && string.endsWith("]")) 311 return string.substring(1, string.length() - 1); 312 return string; 313 }catch (Exception e){ 314 return string; 315 } 316 } 317 318// public static List<String> parseNames(String nameString, EDbVendor vendor) { 319// List<String> names = new ArrayList<String>(); 320// if (nameString.startsWith("`") && nameString.endsWith("`")){ 321// nameString = nameString.substring(1,nameString.length()-1); 322// } 323// String[] splits = nameString.trim().split("\\."); 324// 325// for (int i = 0; i < splits.length; i++) { 326// String split = splits[i].trim(); 327// if (TSQLEnv.isDelimitedIdentifier(vendor, split) && !TSQLEnv.endsWithDelimitedIdentifier(vendor, split)) { 328// StringBuilder buffer = new StringBuilder(); 329// buffer.append(splits[i]); 330// while (i < splits.length - 1 331// && !TSQLEnv.endsWithDelimitedIdentifier(vendor, split = splits[++i].trim())) { 332// buffer.append("."); 333// buffer.append(splits[i]); 334// } 335// 336// buffer.append("."); 337// buffer.append(splits[i]); 338// 339// names.add(buffer.toString()); 340// continue; 341// } 342// names.add(splits[i]); 343// } 344// return names; 345// } 346 347 public static List<String> parseNames(String nameString) { 348 return parseNames(nameString, null); 349 } 350 351 // Manual dot-split: faster than Pattern.split() for "\\s*\\.\\s*" 352 // Only trims whitespace around dots, NOT leading/trailing whitespace of the whole string 353 private static String[] dotSplit(String s) { 354 java.util.List<String> parts = new java.util.ArrayList<String>(4); 355 int segStart = 0; 356 int len = s.length(); 357 for (int i = 0; i < len; i++) { 358 if (s.charAt(i) == '.') { 359 // Trim whitespace on the left side of the dot 360 int end = i; 361 while (end > segStart && s.charAt(end - 1) <= ' ') end--; 362 parts.add(s.substring(segStart, end)); 363 // Skip whitespace on the right side of the dot 364 segStart = i + 1; 365 while (segStart < len && s.charAt(segStart) <= ' ') segStart++; 366 i = segStart - 1; 367 } 368 } 369 // Last segment: keep trailing whitespace as-is (matches Pattern.split behavior) 370 parts.add(s.substring(segStart, len)); 371 return parts.toArray(new String[0]); 372 } 373 374// Cache for parseNames results - uses ConcurrentHashMap for high concurrency 375 private static final java.util.concurrent.ConcurrentHashMap<String, List<String>> PARSE_NAMES_CACHE = 376 new java.util.concurrent.ConcurrentHashMap<String, List<String>>(256); 377 private static final int MAX_CACHE_SIZE = 10000; 378 private static final int CACHE_EVICT_BATCH = 1000; 379 380 /** 381 * 解析以点号分隔的 SQL 标识符或表达式,并返回各层级片段。 382 * <p> 383 * 用途: 384 * <ul> 385 * <li>将类似 catalog.schema.table.column 的全限定名拆分为有序片段;</li> 386 * <li>在拆分时识别并保留引号/括号内的点号,不做误分割;</li> 387 * <li>根据不同数据库厂商(如 BigQuery)的定界符规则进行处理。</li> 388 * </ul> 389 * 工作机制: 390 * <ol> 391 * <li>按「可选空格 + '.' + 可选空格」初步切分;</li> 392 * <li>对以下情况进行片段合并直至遇到闭合符号: 393 * <ul> 394 * <li>单引号字符串:'...'</li> 395 * <li>双引号定界标识符:"..."</li> 396 * <li>反引号定界标识符:`...`</li> 397 * <li>方括号定界标识符:[...]</li> 398 * <li>函数/表达式括号:(...)</li> 399 * </ul> 400 * </li> 401 * <li>BigQuery(vendor == dbvbigquery)且包含反引号时,会先去除反引号再进行切分。</li> 402 * <li>入参为 null 返回空列表;发生异常时返回仅包含原始字符串的列表。</li> 403 * </ol> 404 * 示例: 405 * <pre> 406 * "dbo.Employee.Name" -> ["dbo", "Employee", "Name"] 407 * "[Sales DB].[Employee].[Name]" -> ["[Sales DB]", "[Employee]", "[Name]"] 408 * "\"My.Schema\".\"My.Table\"" -> ["\"My.Schema\"", "\"My.Table\""] 409 * "`project.dataset.table`" (BigQuery) -> ["project", "dataset", "table"] 410 * "OPENJSON(aptd.test.ActiviteTypeIDs)" -> ["OPENJSON(aptd.test.ActiviteTypeIDs)"] 411 * </pre> 412 * 413 * @param nameString 待解析的标识符或表达式字符串 414 * @param vendor 数据库厂商(用于处理厂商特定定界符,如 BigQuery 的反引号),可为 null 415 * @return 解析后的片段列表;顺序与层级一致 416 */ 417 public static List<String> parseNames(String nameString, EDbVendor vendor) { 418 if(nameString == null){ 419 return Collections.emptyList(); 420 } 421 422 // Cache lookup - use a simple cache key 423 String cacheKey = vendor == null ? nameString : (vendor.ordinal() + ":" + nameString); 424 List<String> cached = PARSE_NAMES_CACHE.get(cacheKey); 425 if (cached != null) { 426 return new ArrayList<String>(cached); 427 } 428 429 // Only trim when necessary (avoid allocation for already-trimmed strings) 430 String name = nameString; 431 int len = nameString.length(); 432 if (len > 0 && (nameString.charAt(0) <= ' ' || nameString.charAt(len - 1) <= ' ')) { 433 name = nameString.trim(); 434 len = name.length(); 435 } 436 437 // Fast path: simple names without special characters 438 // Check once for all special characters in a single pass, with early exit 439 boolean hasSpecialChar = false; 440 boolean hasSingleQuote = false; 441 boolean hasDoubleQuote = false; 442 boolean hasBacktick = false; 443 boolean hasParenthesis = false; 444 boolean hasBracket = false; 445 int flagsRemaining = 5; // singleQuote, doubleQuote, backtick, parenthesis, bracket 446 447 for (int i = 0; i < len; i++) { 448 char c = name.charAt(i); 449 switch (c) { 450 case '\'': 451 if (!hasSingleQuote) { hasSingleQuote = true; flagsRemaining--; } 452 hasSpecialChar = true; 453 break; 454 case '"': 455 if (!hasDoubleQuote) { hasDoubleQuote = true; flagsRemaining--; } 456 hasSpecialChar = true; 457 break; 458 case '`': 459 if (!hasBacktick) { hasBacktick = true; flagsRemaining--; } 460 hasSpecialChar = true; 461 break; 462 case '(': 463 case ')': 464 if (!hasParenthesis) { hasParenthesis = true; flagsRemaining--; } 465 hasSpecialChar = true; 466 break; 467 case '[': 468 case ']': 469 if (!hasBracket) { hasBracket = true; flagsRemaining--; } 470 hasSpecialChar = true; 471 break; 472 } 473 if (flagsRemaining == 0) { 474 break; 475 } 476 } 477 478 List<String> names = new ArrayList<String>(4); // Pre-size for typical case 479 480 // Handle BigQuery special case 481 if (vendor == EDbVendor.dbvbigquery && hasBacktick) { 482 String[] parts = dotSplit(name.replace("`", "")); 483 for (String part : parts) { 484 names.add(part); 485 } 486 return putCache(cacheKey, names); 487 } 488 489 try { 490 // Fast path: no special characters 491 if (!hasSpecialChar) { 492 String[] splits = dotSplit(nameString); 493 for (String split : splits) { 494 names.add(split); 495 } 496 return putCache(cacheKey, names); 497 } 498 499 // Legacy sentinel: a lone quoted dot means the dot itself 500 if ("'.'".equals(name) || "`.`".equals(name) 501 || "\".\"".equals(name) || "[.]".equals(name)) { 502 names = Arrays.asList("."); 503 return putCache(cacheKey, names); 504 } 505 506 names = segmentQualifiedName(nameString); 507 } catch (Throwable e) { 508 names.clear(); 509 names.add(nameString); 510 } 511 512 return putCache(cacheKey, names); 513 } 514 515 /** 516 * Single-pass, enclosure-state qualified-name segmenter: splits on dots 517 * only at top level, tracking ALL enclosure kinds simultaneously — 518 * {@code '…'}, {@code "…"}, {@code `…`}, {@code […]} (a doubled close 519 * delimiter inside an enclosure is an escaped literal and does not 520 * close it) and parenthesis nesting for expressions. This replaces the 521 * old one-delimiter-type-per-string dispatch, which mis-segmented names 522 * mixing delimiter characters ({@code [O'Reilly.a].dbo} split at the 523 * apostrophe handler before the bracket handler could see it). 524 * 525 * <p>An unterminated enclosure swallows the rest of the string into the 526 * current segment (the historical behavior malformed inputs relied on). 527 * A closed enclosure that OPENED the segment may only be followed by 528 * optional whitespace and a continuation character 529 * ({@link #isQualifiedNameContinuation}: dot, {@code @dblink}, another 530 * enclosure) or the end of the string — anything else (operators, bare 531 * identifier text) means the input is an EXPRESSION, not a qualified 532 * name, and the whole string is returned as one segment. This is 533 * historical contract: dlineage names expression pseudo-columns by the 534 * full expression text ({@code 'inserted ' || id.*::text}), and 535 * segment-splitting such text truncated lineage display names (U5 536 * dataflow-golden gate, postgresql/IC8XB0). An enclosure opening 537 * MID-segment gets no such check — {@code SCHEMA_NAME(id) + '.' + 538 * st.name} splits with every segment quote-balanced, the MantisBT 4496 539 * contract. Whitespace around TOP-LEVEL dots is trimmed exactly like 540 * {@link #dotSplit}; text inside enclosures is preserved verbatim. 541 */ 542 private static List<String> segmentQualifiedName(String s) { 543 List<String> names = new ArrayList<String>(4); 544 int len = s.length(); 545 int segStart = 0; 546 int parenDepth = 0; 547 char enclosing = 0; // 0 = none; otherwise the OPEN char: ' " ` [ 548 // STICKY per segment: true once an enclosure opens with nothing but 549 // whitespace before it in the segment. Leading whitespace must not 550 // defeat the check (the input arrives untrimmed), and a later 551 // mid-segment opening must not clear it ("q" "r"x.s is still a 552 // quote-led segment gone wrong) — Codex U5 round-2 finding 1. 553 boolean segmentQuoteLed = false; 554 for (int i = 0; i < len; i++) { 555 char c = s.charAt(i); 556 if (enclosing != 0) { 557 char close = enclosing == '[' ? ']' : enclosing; 558 if (c == close) { 559 if (i + 1 < len && s.charAt(i + 1) == close) { 560 i++; // doubled close delimiter: escaped literal 561 } else { 562 enclosing = 0; 563 if (parenDepth == 0 && segmentQuoteLed) { 564 int j = i + 1; 565 while (j < len && s.charAt(j) <= ' ') { 566 j++; 567 } 568 if (j < len && !isQualifiedNameContinuation(s.charAt(j))) { 569 // A segment that IS a quoted form, followed by 570 // operator or bare identifier text: the input 571 // is an expression, not a qualified name. 572 names.clear(); 573 names.add(s); 574 return names; 575 } 576 } 577 } 578 } 579 continue; 580 } 581 switch (c) { 582 case '\'': 583 case '"': 584 case '`': 585 case '[': 586 enclosing = c; 587 // A quote opening MID-segment (after non-whitespace 588 // content, e.g. the '.' literal in SCHEMA_NAME(id) + '.' 589 // + st.name, MantisBT 4496) keeps the historical 590 // split-and-merge behavior; only a segment whose first 591 // non-whitespace text IS a quoted form claims the whole 592 // input when followed by expression text. That includes 593 // the vendor-prefixed openers — PostgreSQL U&"…"/u&"…" 594 // and Power Query #"…" — whose quote char sits 1-2 chars 595 // after the segment start (Codex U5 round-3 finding 1). 596 if (!segmentQuoteLed && parenDepth == 0) { 597 int k = segStart; 598 while (k < i && s.charAt(k) <= ' ') { 599 k++; 600 } 601 segmentQuoteLed = (k == i) 602 || (c == '"' && i - k == 2 603 && (s.charAt(k) == 'U' || s.charAt(k) == 'u') 604 && s.charAt(k + 1) == '&') 605 || (c == '"' && i - k == 1 && s.charAt(k) == '#'); 606 } 607 break; 608 case '(': 609 parenDepth++; 610 break; 611 case ')': 612 if (parenDepth > 0) { 613 parenDepth--; 614 } 615 break; 616 case '.': 617 if (parenDepth == 0) { 618 // trim whitespace on the left side of the dot 619 int end = i; 620 while (end > segStart && s.charAt(end - 1) <= ' ') { 621 end--; 622 } 623 names.add(s.substring(segStart, end)); 624 // skip whitespace on the right side of the dot 625 segStart = i + 1; 626 while (segStart < len && s.charAt(segStart) <= ' ') { 627 segStart++; 628 } 629 i = segStart - 1; 630 segmentQuoteLed = false; // new segment, new state 631 } 632 break; 633 default: 634 break; 635 } 636 } 637 names.add(s.substring(segStart, len)); 638 return names; 639 } 640 641 /** 642 * May this character follow a CLOSED enclosure inside a well-formed 643 * qualified name (after optional whitespace)? The set is the historical 644 * contract measured against the pre-U2b segmenter: a top-level dot 645 * (segment boundary), an Oracle {@code @dblink} continuation 646 * ({@code HR."EMPLOYEES"@"LD_PDB1.LOCALDOMAIN"}), or another enclosure 647 * opening ({@code "q" "r".s}). Operators and bare identifier text 648 * ({@code 'a'||b}, {@code "a"x}, {@code [a]b}) mean the whole input is an 649 * expression, which callers expect back as ONE unsplit segment. 650 */ 651 private static boolean isQualifiedNameContinuation(char c) { 652 return c == '.' || c == '@' || c == '\'' || c == '"' || c == '`' || c == '['; 653 } 654 655 /** 656 * Return the cached parse result for internal read-only consumers. 657 * 658 * <p>{@link #parseNames(String, EDbVendor)} intentionally returns a copy on 659 * a cache hit because callers have historically been free to mutate the 660 * returned list. Identifier normalization only reads the segments, so 661 * copying the same cached list for every column is unnecessary.</p> 662 * 663 * <p>To make skipping the copy provably safe, this method always returns an 664 * <b>unmodifiable</b> list. On a cache hit that is a zero-copy read-only 665 * <i>view</i> over the shared cached list, so callers cannot corrupt the 666 * cache (any mutating call throws) while still avoiding the defensive copy. 667 * This is why the method is restricted to read-only internal consumers.</p> 668 */ 669 private static List<String> parseNamesReadOnly(String nameString, EDbVendor vendor) { 670 if (nameString == null) { 671 return Collections.emptyList(); 672 } 673 674 String cacheKey = vendor == null ? nameString : (vendor.ordinal() + ":" + nameString); 675 List<String> cached = PARSE_NAMES_CACHE.get(cacheKey); 676 if (cached != null) { 677 return Collections.unmodifiableList(cached); 678 } 679 680 List<String> parsed = parseNames(nameString, vendor); 681 cached = PARSE_NAMES_CACHE.get(cacheKey); 682 return Collections.unmodifiableList(cached != null ? cached : new ArrayList<String>(parsed)); 683 } 684 685 protected static List<String> putCache(String cacheKey, List<String> names) { 686 // Simple eviction: when cache exceeds limit, remove a batch of entries 687 if (PARSE_NAMES_CACHE.size() >= MAX_CACHE_SIZE) { 688 java.util.Iterator<String> it = PARSE_NAMES_CACHE.keySet().iterator(); 689 for (int i = 0; i < CACHE_EVICT_BATCH && it.hasNext(); i++) { 690 it.next(); 691 it.remove(); 692 } 693 } 694 // Store immutable copy in cache, but return a mutable copy to the caller 695 PARSE_NAMES_CACHE.put(cacheKey, Collections.unmodifiableList(new ArrayList<String>(names))); 696 return new ArrayList<String>(names); 697 } 698 699 700 /** 701 * A delimited segment is truly CLOSED only when it ends with an ODD run 702 * of the close delimiter: every vendor whose quoted identifiers this 703 * splitter merges escapes the close delimiter by doubling it, so a 704 * trailing even run is escaped literal content and the segment 705 * continues ({@code [a]]} is an open bracket segment whose content so 706 * far is {@code a]}; {@code [a]]]} is closed with content {@code a]}). 707 * 708 * @param seg the (trimmed) piece to examine 709 * @param contentFrom index where content starts (1 for the piece that 710 * carries the opening delimiter, 0 for merge pieces) 711 * @param endDelim the close delimiter 712 */ 713 714 715 716 public static void main(String[] args) { 717 SQLUtil.parseNames("OPENJSON(aptd.test.ActiviteTypeIDs)"); 718 } 719 720 public static void writeToFile(File file, InputStream source, boolean close) { 721 BufferedInputStream bis = null; 722 BufferedOutputStream fouts = null; 723 try { 724 bis = new BufferedInputStream(source); 725 if (!file.exists()) { 726 if (!file.getParentFile().exists()) { 727 file.getParentFile().mkdirs(); 728 } 729 file.createNewFile(); 730 } 731 fouts = new BufferedOutputStream(new FileOutputStream(file)); 732 byte b[] = new byte[1024]; 733 int i = 0; 734 while ((i = bis.read(b)) != -1) { 735 fouts.write(b, 0, i); 736 } 737 fouts.flush(); 738 fouts.close(); 739 if (close) 740 bis.close(); 741 } catch (IOException e) { 742 logger.error("Write file failed.", e); 743 try { 744 if (fouts != null) 745 fouts.close(); 746 } catch (IOException f) { 747 logger.error("Close output stream failed.", f); 748 } 749 if (close) { 750 try { 751 if (bis != null) 752 bis.close(); 753 } catch (IOException f) { 754 logger.error("Close input stream failed.", f); 755 } 756 } 757 } 758 } 759 760 public static void writeToFile(File file, String string) throws IOException { 761 762 if (!file.exists()) { 763 if (!file.getParentFile().exists()) { 764 file.getParentFile().mkdirs(); 765 } 766 file.createNewFile(); 767 } 768 PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file))); 769 if (string != null) 770 out.print(string); 771 out.close(); 772 } 773 774 public static void appendToFile(File file, String string) throws IOException { 775 776 if (!file.exists()) { 777 if (!file.getParentFile().exists()) { 778 file.getParentFile().mkdirs(); 779 } 780 file.createNewFile(); 781 } 782 PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file, true))); 783 if (string != null) 784 out.println(string); 785 out.close(); 786 } 787 788 public static void deltree(File root) { 789 if (root == null || !root.exists()) { 790 return; 791 } 792 793 if (root.isFile()) { 794 root.delete(); 795 return; 796 } 797 798 File[] children = root.listFiles(); 799 if (children != null) { 800 for (int i = 0; i < children.length; i++) { 801 deltree(children[i]); 802 } 803 } 804 805 root.delete(); 806 } 807 808 public static InputStream getInputStreamWithoutBom(String file) throws IOException { 809 UnicodeInputStream stream = null; 810 FileInputStream fis = new FileInputStream(file); 811 stream = new UnicodeInputStream(fis, null); 812 return stream; 813 } 814 815 public static class UnicodeInputStream extends InputStream { 816 PushbackInputStream internalIn; 817 boolean isInited = false; 818 String defaultEnc; 819 String encoding; 820 821 private static final int BOM_SIZE = 4; 822 823 public UnicodeInputStream(InputStream in, String defaultEnc) { 824 internalIn = new PushbackInputStream(in, BOM_SIZE); 825 this.defaultEnc = defaultEnc; 826 } 827 828 public String getDefaultEncoding() { 829 return defaultEnc; 830 } 831 832 public String getEncoding() { 833 if (!isInited) { 834 try { 835 init(); 836 } catch (IOException ex) { 837 IllegalStateException ise = new IllegalStateException("Init method failed."); 838 ise.initCause(ise); 839 throw ise; 840 } 841 } 842 return encoding; 843 } 844 845 protected void init() throws IOException { 846 if (isInited) 847 return; 848 849 byte bom[] = new byte[BOM_SIZE]; 850 int n, unread; 851 n = internalIn.read(bom, 0, bom.length); 852 853 if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) 854 && (bom[3] == (byte) 0xFF)) { 855 encoding = "UTF-32BE"; 856 unread = n - 4; 857 } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) 858 && (bom[3] == (byte) 0x00)) { 859 encoding = "UTF-32LE"; 860 unread = n - 4; 861 } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { 862 encoding = "UTF-8"; 863 unread = n - 3; 864 } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { 865 encoding = "UTF-16BE"; 866 unread = n - 2; 867 } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { 868 encoding = "UTF-16LE"; 869 unread = n - 2; 870 } else { 871 encoding = defaultEnc; 872 unread = n; 873 } 874 875 if (unread > 0) 876 internalIn.unread(bom, (n - unread), unread); 877 878 isInited = true; 879 } 880 881 public void close() throws IOException { 882 internalIn.close(); 883 } 884 885 public int read() throws IOException { 886 return internalIn.read(); 887 } 888 } 889 890 /** 891 * Single-segment SQL identifier equality — the unified façade for 892 * identifier-vs-identifier name comparisons (see 893 * docs/refactor/identifier_normalization_unification_plan.md). 894 * 895 * <p>Both arguments must be a single name segment (no {@code a.b} qualification); 896 * qualified names are segmented by {@link #compareIdentifier(EDbVendor, 897 * ESQLDataObjectType, String, String)}, which compares segment by segment. 898 * 899 * <p><strong>POLICY: CANONICAL (since P0d.2).</strong> Equality is canonical-key 900 * equality ({@link #canonKey}): each operand is quote-stripped and folded by its own 901 * quote state under the vendor's identifier rules, then compared exactly. The 902 * relation is an equivalence relation, safe as a map-key contract, and matches how 903 * names are stored (Oracle unquoted {@code foo} ≡ quoted {@code "FOO"}, never 904 * {@code "foo"}). 905 * 906 * @param dbVendor database vendor whose identifier rules apply 907 * @param objectType kind of database object the names refer to 908 * @param ident1 first identifier segment (may be quoted) 909 * @param ident2 second identifier segment (may be quoted) 910 * @return true if the two names refer to the same object under the vendor's 911 * canonical comparison rules 912 */ 913 public static boolean sameName(EDbVendor dbVendor, ESQLDataObjectType objectType, 914 String ident1, String ident2) { 915 return TSQLEnv.compareIdentifier(dbVendor, objectType, ident1, ident2); 916 } 917 918 /** 919 * Canonical identity key of a single identifier segment — key equality is exactly 920 * {@link #sameName} (see {@link gudusoft.gsqlparser.sqlenv.CanonKey}; in-process 921 * only, never persist). 922 */ 923 public static gudusoft.gsqlparser.sqlenv.CanonKey canonKey(EDbVendor dbVendor, 924 ESQLDataObjectType objectType, String identifier) { 925 return IdentifierService.canonKeyStatic(dbVendor, objectType, identifier); 926 } 927 928 /** 929 * Canonical identity key of a possibly-qualified name <strong>as written in SQL</strong> 930 * (see {@link gudusoft.gsqlparser.sqlenv.QualifiedCanonKey}; in-process only, never 931 * persist). The provenance is in the name on purpose: a name already in the form a 932 * catalog stores must go through 933 * {@link IdentifierService#qualifiedCanonKeyFromStored(java.util.List, 934 * ESQLDataObjectType)} instead, because the two are not interchangeable. 935 * 936 * <p>Returns null for a null or empty name; throws 937 * {@link UnsupportedOperationException} for an object type with no hierarchy mapping. 938 */ 939 public static gudusoft.gsqlparser.sqlenv.QualifiedCanonKey qualifiedCanonKeyFromLexical( 940 EDbVendor dbVendor, ESQLDataObjectType objectType, String qualifiedName) { 941 return IdentifierService.qualifiedCanonKeyFromLexicalStatic(dbVendor, objectType, qualifiedName); 942 } 943 944 public static boolean compareIdentifier(EDbVendor dbVendor, ESQLDataObjectType sqlDataObjectType, 945 String identifier1, String identifier2) { 946 return compareQualified(dbVendor, sqlDataObjectType, identifier1, identifier2, 947 (type, a, b) -> TSQLEnv.compareIdentifier(dbVendor, type, a, b)); 948 } 949 950 /** 951 * {@link #compareIdentifier(EDbVendor, ESQLDataObjectType, String, String)} under the rules of 952 * one analysis rather than its vendor's defaults: the same segmentation, each segment compared by 953 * {@code service}. A lineage analysis configured with its own {@link IdentifierProfile} must 954 * compare qualified names by it, or its keys and its comparisons follow two different rules. 955 */ 956 public static boolean compareIdentifier(IdentifierService service, ESQLDataObjectType sqlDataObjectType, 957 String identifier1, String identifier2) { 958 return compareQualified(service.getProfile().getVendor(), sqlDataObjectType, identifier1, identifier2, 959 (type, a, b) -> service.areEqual(a, b, type)); 960 } 961 962 /** Equality of one name segment in the role its position gives it. */ 963 private interface SegmentEquality { 964 boolean same(ESQLDataObjectType type, String a, String b); 965 } 966 967 private static boolean compareQualified(EDbVendor dbVendor, ESQLDataObjectType sqlDataObjectType, 968 String identifier1, String identifier2, 969 SegmentEquality segmentEquality) { 970 List<String> segments1 = parseNames(identifier1); 971 List<String> segments2 = parseNames(identifier2); 972 973 if (segments1.size() != segments2.size()) 974 return false; 975 976 boolean supportCatalog = TSQLEnv.supportCatalog(dbVendor); 977 boolean supportSchema = TSQLEnv.supportSchema(dbVendor); 978 979 if(supportCatalog && supportSchema) { 980 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 981 if (segments1.size() > 4) { 982 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 983 segments2.get(0)) 984 && segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(1), 985 segments2.get(1)) 986 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(2), 987 segments2.get(2)) 988 && segmentEquality.same(ESQLDataObjectType.dotColumn, mergeSegments(segments1, 3), 989 mergeSegments(segments2, 3)); 990 } else if (segments1.size() == 4) { 991 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 992 segments2.get(0)) 993 && segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(1), 994 segments2.get(1)) 995 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(2), 996 segments2.get(2)) 997 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(3), 998 segments2.get(3)); 999 } else if (segments1.size() == 3) { 1000 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1001 segments2.get(0)) 1002 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(1), 1003 segments2.get(1)) 1004 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(2), 1005 segments2.get(2)); 1006 } else if (segments1.size() == 2) { 1007 return segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(0), 1008 segments2.get(0)) 1009 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(1), 1010 segments2.get(1)); 1011 } else if (segments1.size() == 1) { 1012 return segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(0), 1013 segments2.get(0)); 1014 } 1015 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1016 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1017 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1018 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1019 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1020 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1021 if (segments1.size() > 3) { 1022 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1023 segments2.get(0)) 1024 && segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(1), 1025 segments2.get(1)) 1026 && segmentEquality.same(sqlDataObjectType, mergeSegments(segments1, 2), mergeSegments(segments2, 2)); 1027 } else if (segments1.size() == 3) { 1028 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1029 segments2.get(0)) 1030 && segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(1), 1031 segments2.get(1)) 1032 && segmentEquality.same(sqlDataObjectType, segments1.get(2), segments2.get(2)); 1033 } else if (segments1.size() == 2) { 1034 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1035 segments2.get(0)) 1036 && segmentEquality.same(sqlDataObjectType, segments1.get(1), segments2.get(1)); 1037 } else if (segments1.size() == 1) { 1038 return segmentEquality.same(sqlDataObjectType, segments1.get(0), segments2.get(0)); 1039 } 1040 } else if (sqlDataObjectType == ESQLDataObjectType.dotSchema) { 1041 if (segments1.size() > 2) { 1042 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1043 segments2.get(0)) 1044 && segmentEquality.same(sqlDataObjectType, mergeSegments(segments1, 1), mergeSegments(segments2, 1)); 1045 } else if (segments1.size() == 2) { 1046 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1047 segments2.get(0)) 1048 && segmentEquality.same(sqlDataObjectType, segments1.get(1), segments2.get(1)); 1049 } else if (segments1.size() == 1) { 1050 return segmentEquality.same(sqlDataObjectType, segments1.get(0), segments2.get(0)); 1051 } 1052 } else if (sqlDataObjectType == ESQLDataObjectType.dotCatalog) { 1053 if (segments1.size() > 1) { 1054 return segmentEquality.same(sqlDataObjectType, mergeSegments(segments1, 0), mergeSegments(segments2, 0)); 1055 } else if (segments1.size() == 1) { 1056 return segmentEquality.same(sqlDataObjectType, segments1.get(0), segments2.get(0)); 1057 } 1058 } 1059 } 1060 else if(supportCatalog){ 1061 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 1062 if (segments1.size() > 3) { 1063 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1064 segments2.get(0)) 1065 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(1), 1066 segments2.get(1)) 1067 && segmentEquality.same(ESQLDataObjectType.dotColumn, mergeSegments(segments1, 2), 1068 mergeSegments(segments2, 2)); 1069 } else if (segments1.size() == 3) { 1070 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1071 segments2.get(0)) 1072 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(1), 1073 segments2.get(1)) 1074 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(2), 1075 segments2.get(2)); 1076 } else if (segments1.size() == 2) { 1077 return segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(0), 1078 segments2.get(0)) 1079 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(1), 1080 segments2.get(1)); 1081 } else if (segments1.size() == 1) { 1082 return segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(0), 1083 segments2.get(0)); 1084 } 1085 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1086 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1087 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1088 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1089 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1090 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1091 if (segments1.size() > 2) { 1092 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1093 segments2.get(0)) 1094 && segmentEquality.same(sqlDataObjectType, mergeSegments(segments1, 1), mergeSegments(segments2, 1)); 1095 } else if (segments1.size() == 2) { 1096 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), 1097 segments2.get(0)) 1098 && segmentEquality.same(sqlDataObjectType, segments1.get(1), segments2.get(1)); 1099 } else if (segments1.size() == 1) { 1100 return segmentEquality.same(sqlDataObjectType, segments1.get(0), segments2.get(0)); 1101 } 1102 } else if (sqlDataObjectType == ESQLDataObjectType.dotCatalog || sqlDataObjectType == ESQLDataObjectType.dotSchema) { 1103 if (segments1.size() > 1) { 1104 return segmentEquality.same(ESQLDataObjectType.dotCatalog, mergeSegments(segments1, 0), mergeSegments(segments2, 0)); 1105 } else if (segments1.size() == 1) { 1106 return segmentEquality.same(ESQLDataObjectType.dotCatalog, segments1.get(0), segments2.get(0)); 1107 } 1108 } 1109 } 1110 else if(supportSchema){ 1111 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 1112 if (segments1.size() > 3) { 1113 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1114 segments2.get(0)) 1115 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(1), 1116 segments2.get(1)) 1117 && segmentEquality.same(ESQLDataObjectType.dotColumn, mergeSegments(segments1, 2), 1118 mergeSegments(segments2, 2)); 1119 } else if (segments1.size() == 3) { 1120 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1121 segments2.get(0)) 1122 && segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(1), 1123 segments2.get(1)) 1124 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(2), 1125 segments2.get(2)); 1126 } else if (segments1.size() == 2) { 1127 return segmentEquality.same(ESQLDataObjectType.dotTable, segments1.get(0), 1128 segments2.get(0)) 1129 && segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(1), 1130 segments2.get(1)); 1131 } else if (segments1.size() == 1) { 1132 return segmentEquality.same(ESQLDataObjectType.dotColumn, segments1.get(0), 1133 segments2.get(0)); 1134 } 1135 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1136 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1137 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1138 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1139 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1140 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1141 if (segments1.size() > 2) { 1142 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1143 segments2.get(0)) 1144 && segmentEquality.same(sqlDataObjectType, mergeSegments(segments1, 1), mergeSegments(segments2, 1)); 1145 } else if (segments1.size() == 2) { 1146 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), 1147 segments2.get(0)) 1148 && segmentEquality.same(sqlDataObjectType, segments1.get(1), segments2.get(1)); 1149 } else if (segments1.size() == 1) { 1150 return segmentEquality.same(sqlDataObjectType, segments1.get(0), segments2.get(0)); 1151 } 1152 } else if (sqlDataObjectType == ESQLDataObjectType.dotCatalog || sqlDataObjectType == ESQLDataObjectType.dotSchema) { 1153 if (segments1.size() > 1) { 1154 return segmentEquality.same(ESQLDataObjectType.dotSchema, mergeSegments(segments1, 0), mergeSegments(segments2, 0)); 1155 } else if (segments1.size() == 1) { 1156 return segmentEquality.same(ESQLDataObjectType.dotSchema, segments1.get(0), segments2.get(0)); 1157 } 1158 } 1159 } 1160 return false; 1161 } 1162 1163 /** 1164 * Legacy "normal name" builder: canonical normalize, then collapse to UPPER when the 1165 * (vendor, objectType) domain is case-insensitive — the pre-{@link 1166 * gudusoft.gsqlparser.sqlenv.CanonKey} map-key convention. Multi-segment inputs are returned in their origin (only case-collapsed) 1167 * form. 1168 * 1169 * @deprecated For equality tests use {@link #sameName(EDbVendor, ESQLDataObjectType, 1170 * String, String)} / {@link #compareIdentifier(EDbVendor, ESQLDataObjectType, String, 1171 * String)} — never normalize-then-compare. For map keys use {@link #canonKey(EDbVendor, 1172 * ESQLDataObjectType, String)} (or {@link IdentifierService#keyForMap(String, 1173 * ESQLDataObjectType)}); for the stored/display form use 1174 * {@link IdentifierService#normalizeStatic(EDbVendor, ESQLDataObjectType, String)}. 1175 * Retained only for callers that persist the legacy normal form (e.g. dlineage 1176 * display names — see the identifier normalization guide §6). 1177 */ 1178 @Deprecated 1179 public static String normalizeIdentifier(EDbVendor dbVendor, ESQLDataObjectType objectType, String identifier) { 1180 if (identifier == null) 1181 return null; 1182 String legacy = legacyNormalSegment(dbVendor, objectType, identifier); 1183 if (legacy == null) { 1184 return legacyCollationSensitive(dbVendor, objectType) ? identifier : identifier.toUpperCase(Locale.ROOT); 1185 } 1186 // A lineage analysis gives two different names that share this string names of their own. 1187 ModelBindingManager manager = ModelBindingManager.get(); 1188 return manager == null ? legacy : manager.normalName(dbVendor, objectType, identifier, legacy); 1189 } 1190 1191 /** 1192 * The legacy normal name of ONE identifier segment: {@link IdentifierService#normalizeStatic} 1193 * upper-cased wherever the legacy collation maps call the role case-insensitive. Different 1194 * names can share it (PostgreSQL {@code "Amount"} and {@code amount} both give {@code AMOUNT}); 1195 * {@link #normalizeIdentifier} is what a lineage analysis keeps collision-free. 1196 * 1197 * @return the legacy name, or {@code null} when the text is not a single segment 1198 */ 1199 public static String legacyNormalSegment(EDbVendor dbVendor, ESQLDataObjectType objectType, String identifier) { 1200 if (identifier == null) 1201 return null; 1202 String normalized = IdentifierService.normalizeStatic(dbVendor, objectType, identifier); 1203 if (parseNamesReadOnly(normalized, null).size() > 1) { 1204 return null; 1205 } 1206 return legacyCollationSensitive(dbVendor, objectType) ? normalized : normalized.toUpperCase(Locale.ROOT); 1207 } 1208 1209 /** 1210 * Which legacy collation map decides the case of a role's normal name: roles in one class 1211 * always get the same legacy name for the same text. 1212 */ 1213 public static String legacyFoldClass(ESQLDataObjectType objectType) { 1214 switch (objectType) { 1215 case dotCatalog: 1216 case dotSchema: 1217 return "catalog"; 1218 case dotFunction: 1219 case dotProcedure: 1220 case dotTrigger: 1221 case dotTable: 1222 case dotSynonyms: 1223 case dotOraclePackage: 1224 return "table"; 1225 case dotColumn: 1226 return "column"; 1227 default: 1228 return "default"; 1229 } 1230 } 1231 1232 private static boolean legacyCollationSensitive(EDbVendor dbVendor, ESQLDataObjectType objectType) { 1233 switch (legacyFoldClass(objectType)) { 1234 case "catalog": 1235 return TSQLEnv.catalogCollationCaseSensitive.get(dbVendor); 1236 case "table": 1237 return TSQLEnv.tableCollationCaseSensitive.get(dbVendor); 1238 case "column": 1239 return TSQLEnv.columnCollationCaseSensitive.get(dbVendor); 1240 default: 1241 return TSQLEnv.defaultCollationCaseSensitive.get(dbVendor); 1242 } 1243 } 1244 1245 /** 1246 * Legacy display/normal-name form of a column reference. NOT an equality primitive: 1247 * comparing two normal names with {@code equals}/{@code equalsIgnoreCase} is the 1248 * pre-P0d bug pattern — use {@link #sameName(EDbVendor, ESQLDataObjectType, String, 1249 * String)} / {@link #compareIdentifier(EDbVendor, ESQLDataObjectType, String, String)} 1250 * instead. 1251 */ 1252 public static String getIdentifierNormalColumnName(EDbVendor dbVendor, String name) { 1253 return getIdentifierNormalName(dbVendor, name, ESQLDataObjectType.dotColumn); 1254 } 1255 1256 /** 1257 * Legacy display/normal-name form of a table reference (global analysis vendor). 1258 * NOT an equality primitive — for equality use {@link #sameName(EDbVendor, 1259 * ESQLDataObjectType, String, String)} / {@link #compareIdentifier(EDbVendor, 1260 * ESQLDataObjectType, String, String)}, never string compares on this result. 1261 */ 1262 public static String getIdentifierNormalTableName(String name) { 1263 return getIdentifierNormalName(ModelBindingManager.getGlobalVendor(), name, ESQLDataObjectType.dotTable); 1264 } 1265 1266 /** 1267 * Legacy display/normal-name form of a table reference. NOT an equality primitive — 1268 * for equality use {@link #sameName(EDbVendor, ESQLDataObjectType, String, String)} / 1269 * {@link #compareIdentifier(EDbVendor, ESQLDataObjectType, String, String)}. 1270 */ 1271 public static String getIdentifierNormalTableName(EDbVendor dbVendor, String name) { 1272 return getIdentifierNormalName(dbVendor, name, ESQLDataObjectType.dotTable); 1273 } 1274 1275 /** 1276 * 规范化多段限定名(Multi-Segment Qualified Name)并返回规范化后的完整限定名。 1277 * 1278 * <h3>核心功能</h3> 1279 * <p>本方法是 SQL 标识符规范化的<b>多段名处理版本</b>,接受包含多段(catalog.schema.table.column)的限定名, 1280 * 根据数据库厂商特性、层级支持能力和对象类型,智能解析和规范化每个段,最后返回用点号连接的完整规范化名称。</p> 1281 * 1282 * <h3>处理流程</h3> 1283 * <ol> 1284 * <li><b>厂商特定语法展开</b>: 1285 * <ul> 1286 * <li>MSSQL/Azure SQL: 将 ".." 语法展开为 ".dbo." 或全局配置的 schema</li> 1287 * <li>示例:{@code "db..table"} → {@code "db.dbo.table"}</li> 1288 * </ul> 1289 * </li> 1290 * <li><b>解析多段名</b>:使用 {@link #parseNames(String)} 将限定名按点号分割成段列表</li> 1291 * <li><b>智能段类型推断</b>:根据以下因素决定每段的实际类型: 1292 * <ul> 1293 * <li>数据库厂商的层级支持能力(supportCatalog/supportSchema)</li> 1294 * <li>目标对象类型({@code sqlDataObjectType})</li> 1295 * <li>实际段数</li> 1296 * </ul> 1297 * </li> 1298 * <li><b>逐段规范化</b>:对每段调用 {@link #normalizeIdentifier(EDbVendor, ESQLDataObjectType, String)}, 1299 * 应用厂商特定的大小写规则、引号处理等</li> 1300 * <li><b>重组限定名</b>:用点号连接所有规范化后的段,返回完整的规范化限定名</li> 1301 * </ol> 1302 * 1303 * <h3>智能段类型推断示例</h3> 1304 * <p>假设数据库<b>同时支持 catalog 和 schema</b>,目标类型为 {@code dotTable}:</p> 1305 * <ul> 1306 * <li>3段名 {@code "a.b.c"} → 解析为 {@code catalog.schema.table}</li> 1307 * <li>2段名 {@code "a.b"} → 解析为 {@code schema.table}</li> 1308 * <li>1段名 {@code "a"} → 解析为 {@code table}</li> 1309 * </ul> 1310 * 1311 * <p>假设数据库<b>仅支持 catalog</b>(如 MySQL),目标类型为 {@code dotTable}:</p> 1312 * <ul> 1313 * <li>2段名 {@code "a.b"} → 解析为 {@code catalog.table}</li> 1314 * <li>1段名 {@code "a"} → 解析为 {@code table}</li> 1315 * </ul> 1316 * 1317 * <p>假设数据库<b>仅支持 schema</b>(如 PostgreSQL),目标类型为 {@code dotTable}:</p> 1318 * <ul> 1319 * <li>2段名 {@code "a.b"} → 解析为 {@code schema.table}</li> 1320 * <li>1段名 {@code "a"} → 解析为 {@code table}</li> 1321 * </ul> 1322 * 1323 * <h3>对于 dotColumn 类型的特殊处理</h3> 1324 * <p>列名可能有4段(catalog.schema.table.column),方法会根据实际段数自动调整解析策略:</p> 1325 * <ul> 1326 * <li>4段名 {@code "db.sch.tbl.col"} → {@code catalog.schema.table.column}</li> 1327 * <li>3段名 {@code "sch.tbl.col"} → {@code schema.table.column}</li> 1328 * <li>2段名 {@code "tbl.col"} → {@code table.column}</li> 1329 * <li>1段名 {@code "col"} → {@code column}</li> 1330 * </ul> 1331 * 1332 * <h3>处理超长限定名</h3> 1333 * <p>当段数超过标准层级时(如表名有4段或更多),方法会使用 {@link #mergeSegments(List, int)} 1334 * 将多余的尾部段合并为单个段(保留点号),然后作为最后一段进行规范化。</p> 1335 * <p>示例:5段列名 {@code "db.sch.tbl.nested.col"} → 合并后4段处理: 1336 * {@code "db.sch.tbl.nested.col"} (最后两段合并为 {@code "nested.col"})</p> 1337 * 1338 * <h3>方法目的与使用场景</h3> 1339 * <ul> 1340 * <li><b>目的</b>:为多段限定名提供统一的规范化接口,生成可用于比较、索引和查找的标准化名称</li> 1341 * <li><b>输出特性</b>:返回<b>多段名</b>(用点号连接),保留层级结构信息</li> 1342 * <li><b>使用场景</b>: 1343 * <ul> 1344 * <li>环境层(TSQLEnv)处理完整限定名</li> 1345 * <li>名称比较和匹配(需要保留层级信息)</li> 1346 * <li>快速索引查找(NameKey 构造)</li> 1347 * </ul> 1348 * </li> 1349 * </ul> 1350 * 1351 * <h3>与相关方法的区别</h3> 1352 * <ul> 1353 * <li><b>{@link #normalizeIdentifier(EDbVendor, ESQLDataObjectType, String)}</b>: 1354 * 单段规范化,不解析限定名,直接处理引号和大小写,返回单段名</li> 1355 * <li><b>{@code IdentifierService.keyForMap(String, ESQLDataObjectType)}</b>: 1356 * 仅接受单段名,用于生成 Map 的 key,会抛出异常如果输入是多段名</li> 1357 * <li><b>{@code IdentifierService.normalizeQualifiedName(String, ESQLDataObjectType)}</b>: 1358 * 新架构中的等价方法,提供相同的多段名规范化功能</li> 1359 * </ul> 1360 * 1361 * <h3>厂商特定行为</h3> 1362 * <ul> 1363 * <li><b>MySQL</b>: 仅支持 catalog(即 database),反引号引用,大小写根据系统变量决定</li> 1364 * <li><b>PostgreSQL</b>: 仅支持 schema,双引号引用,未加引号的标识符转小写</li> 1365 * <li><b>SQL Server</b>: 支持 catalog+schema,方括号或双引号引用,".." 展开为 ".dbo."</li> 1366 * <li><b>Oracle</b>: 仅支持 schema,双引号引用,未加引号的标识符转大写</li> 1367 * <li><b>Snowflake</b>: 支持 catalog+schema,双引号引用,大小写保留但匹配不敏感</li> 1368 * <li><b>BigQuery</b>: 支持 catalog+schema(项目+数据集),反引号引用,大小写不敏感,可能内部转小写</li> 1369 * </ul> 1370 * 1371 * @param dbVendor 数据库厂商类型(决定层级支持、引号风格、大小写规则) 1372 * @param name 原始标识符或多段限定名(可能包含引号/反引号;可能包含 catalog/schema 前缀; 1373 * 可能包含厂商特定语法如 MSSQL 的 "..") 1374 * @param sqlDataObjectType 期望的对象类型(例如 dotTable, dotColumn, dotSchema, dotCatalog), 1375 * 用于指导段类型推断和规范化规则应用 1376 * @return 规范化后的完整限定名,用点号连接各段;如果输入为 {@code null},返回 {@code null}; 1377 * 如果输入为空字符串或无法解析,返回空字符串 1378 * 1379 * <p><b>等价性警告(equality warning)</b>:本方法产出的是显示/存储用的规范名, 1380 * <b>不是</b>等价比较原语。判断两个名字是否指向同一数据库对象,请使用 1381 * {@link #compareIdentifier(EDbVendor, ESQLDataObjectType, String, String)}(多段限定名) 1382 * 或 {@link #sameName(EDbVendor, ESQLDataObjectType, String, String)}(单段), 1383 * 不要对两个规范名的返回值做 {@code equals}/{@code equalsIgnoreCase} 比较 —— 那是 P0d 1384 * 统一化之前的 bug 模式。详见 gsp_java_core/doc/user_guide/identifier_normalization_guide.md。 1385 * 1386 * @see #normalizeIdentifier(EDbVendor, ESQLDataObjectType, String) 单段规范化方法 1387 * @see #parseNames(String) 多段名解析方法 1388 * @see #mergeSegments(List, int) 段合并工具方法 1389 * @see TSQLEnv#normalizeIdentifier(EDbVendor, ESQLDataObjectType, String) 1390 * @see IdentifierService#normalizeQualifiedName(String, ESQLDataObjectType) Phase 0 新架构中的等价方法 1391 * 1392 * @since 3.1.0.8 1393 */ 1394 public static String getIdentifierNormalName(EDbVendor dbVendor, String name, 1395 ESQLDataObjectType sqlDataObjectType) { 1396 if (name == null) { 1397 return null; 1398 } 1399 if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) { 1400 if (name.indexOf("..") != -1) { 1401 if (ModelBindingManager.getGlobalSchema() != null) { 1402 name = name.replace("..", "." + ModelBindingManager.getGlobalSchema() + "."); 1403 } else { 1404 name = name.replace("..", ".dbo."); 1405 } 1406 } 1407 } 1408 List<String> segments = parseNamesReadOnly(name, null); 1409 StringBuilder builder = new StringBuilder(); 1410 boolean supportCatalog = TSQLEnv.supportCatalog(dbVendor); 1411 boolean supportSchema = TSQLEnv.supportSchema(dbVendor); 1412 1413 if(supportCatalog && supportSchema) { 1414 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 1415 if (segments.size() > 4) { 1416 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1417 .append(".") 1418 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(1))) 1419 .append(".") 1420 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(2))) 1421 .append(".") 1422 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, mergeSegments(segments, 3))); 1423 } else if (segments.size() == 4) { 1424 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1425 .append(".") 1426 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(1))) 1427 .append(".") 1428 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(2))) 1429 .append(".") 1430 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(3))); 1431 } else if (segments.size() == 3) { 1432 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1433 .append(".") 1434 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(1))) 1435 .append(".") 1436 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(2))); 1437 } else if (segments.size() == 2) { 1438 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(0))) 1439 .append(".") 1440 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(1))); 1441 } else if (segments.size() == 1) { 1442 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(0))); 1443 } 1444 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1445 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1446 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1447 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1448 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1449 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1450 if (segments.size() > 3) { 1451 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1452 .append(".") 1453 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(1))) 1454 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, mergeSegments(segments, 2))); 1455 } else if (segments.size() == 3) { 1456 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1457 .append(".") 1458 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(1))) 1459 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(2))); 1460 } else if (segments.size() == 2) { 1461 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1462 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(1))); 1463 } else if (segments.size() == 1) { 1464 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(0))); 1465 } 1466 } else if (sqlDataObjectType == ESQLDataObjectType.dotSchema) { 1467 if (segments.size() > 2) { 1468 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1469 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, mergeSegments(segments, 1))); 1470 } else if (segments.size() == 2) { 1471 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1472 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(1))); 1473 } else if (segments.size() == 1) { 1474 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(0))); 1475 } 1476 } else if (sqlDataObjectType == ESQLDataObjectType.dotCatalog) { 1477 if (segments.size() > 1) { 1478 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, mergeSegments(segments, 0))); 1479 } else if (segments.size() == 1) { 1480 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(0))); 1481 } 1482 } 1483 } 1484 else if(supportCatalog){ 1485 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 1486 if (segments.size() > 3) { 1487 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1488 .append(".") 1489 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(1))) 1490 .append(".") 1491 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, mergeSegments(segments, 2))); 1492 } else if (segments.size() == 3) { 1493 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1494 .append(".") 1495 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(1))) 1496 .append(".") 1497 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(2))); 1498 } else if (segments.size() == 2) { 1499 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(0))) 1500 .append(".") 1501 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(1))); 1502 } else if (segments.size() == 1) { 1503 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(0))); 1504 } 1505 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1506 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1507 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1508 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1509 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1510 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1511 if (segments.size() > 2) { 1512 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1513 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, mergeSegments(segments, 1))); 1514 } else if (segments.size() == 2) { 1515 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))) 1516 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(1))); 1517 } else if (segments.size() == 1) { 1518 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(0))); 1519 } 1520 } else if (sqlDataObjectType == ESQLDataObjectType.dotSchema || sqlDataObjectType == ESQLDataObjectType.dotCatalog) { 1521 if (segments.size() > 1) { 1522 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, mergeSegments(segments, 0))); 1523 } else if (segments.size() == 1) { 1524 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotCatalog, segments.get(0))); 1525 } 1526 } 1527 } 1528 else if(supportSchema){ 1529 if (sqlDataObjectType == ESQLDataObjectType.dotColumn) { 1530 if (segments.size() > 3) { 1531 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1532 .append(".") 1533 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(1))) 1534 .append(".") 1535 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, mergeSegments(segments, 2))); 1536 } else if (segments.size() == 3) { 1537 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1538 .append(".") 1539 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(1))) 1540 .append(".") 1541 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(2))); 1542 } else if (segments.size() == 2) { 1543 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotTable, segments.get(0))) 1544 .append(".") 1545 .append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(1))); 1546 } else if (segments.size() == 1) { 1547 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotColumn, segments.get(0))); 1548 } 1549 } else if (sqlDataObjectType == ESQLDataObjectType.dotTable 1550 || sqlDataObjectType == ESQLDataObjectType.dotSynonyms 1551 || sqlDataObjectType == ESQLDataObjectType.dotOraclePackage 1552 || sqlDataObjectType == ESQLDataObjectType.dotFunction 1553 || sqlDataObjectType == ESQLDataObjectType.dotProcedure 1554 || sqlDataObjectType == ESQLDataObjectType.dotTrigger) { 1555 if (segments.size() > 2) { 1556 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1557 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, mergeSegments(segments, 1))); 1558 } else if (segments.size() == 2) { 1559 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))) 1560 .append(".").append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(1))); 1561 } else if (segments.size() == 1) { 1562 builder.append(normalizeIdentifier(dbVendor, sqlDataObjectType, segments.get(0))); 1563 } 1564 } else if (sqlDataObjectType == ESQLDataObjectType.dotSchema || sqlDataObjectType == ESQLDataObjectType.dotCatalog) { 1565 if (segments.size() > 1) { 1566 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, mergeSegments(segments, 0))); 1567 } else if (segments.size() == 1) { 1568 builder.append(normalizeIdentifier(dbVendor, ESQLDataObjectType.dotSchema, segments.get(0))); 1569 } 1570 } 1571 } 1572 return builder.toString(); 1573 } 1574 1575 public static String mergeSegments(List<String> segments, int index) { 1576 StringBuilder buffer = new StringBuilder(); 1577 for (int i = index; i < segments.size(); i++) { 1578 buffer.append(segments.get(i)); 1579 if(i<segments.size()-1) { 1580 buffer.append("."); 1581 } 1582 } 1583 return buffer.toString(); 1584 } 1585 1586// public static String getIdentifierNormalName(EDbVendor vendor, String name) { 1587// if (isEmpty(name)) { 1588// return null; 1589// } 1590// name = name.replaceAll("(?i)null\\.", ""); 1591// switch (vendor) { 1592// case dbvbigquery: 1593// return replaceIdentifierNormalName(name, "`.*?`", "`", true).replaceAll("\\.\\s+", "."); 1594// case dbvcouchbase: 1595// case dbvhive: 1596// case dbvimpala: 1597// case dbvmysql: 1598// return replaceIdentifierNormalName(name, "`.*?`", "`", true).replaceAll("\\.\\s+", "."); 1599// case dbvdax: 1600// return replaceIdentifierNormalName(name, "'.*?'", "'", true).replaceAll("\\.\\s+", "."); 1601// case dbvdb2: 1602// case dbvhana: 1603// case dbvinformix: 1604// case dbvnetezza: 1605// case dbvoracle: 1606// case dbvsnowflake: 1607// case dbvsybase: 1608// case dbvteradata: 1609// case dbvvertica: 1610// return replaceIdentifierNormalName(name, "\".*?\"", "\"", true).replaceAll("\\.\\s+", "."); 1611// case dbvpostgresql: 1612// case dbvgreenplum: 1613// case dbvredshift: 1614// return replaceIdentifierNormalName(name, "\".*?\"", "\"", false).replaceAll("\\.\\s+", "."); 1615// case dbvmssql: 1616// return replaceIdentifierNormalName(name, "([\"\\[']).*?([\"\\]'])", "[\"\\[\\]']", true) 1617// .replaceAll("\\.\\s+", "."); 1618// default: 1619// return replaceIdentifierNormalName(name, "\".*?\"", "\"", true).replaceAll("\\.\\s+", "."); 1620// } 1621// } 1622// 1623// private static String replaceIdentifierNormalName(String content, String match, String replace, boolean toUpper) { 1624// Pattern pattern = Pattern.compile(match, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); 1625// Matcher matcher = pattern.matcher(content); 1626// StringBuilder buffer = new StringBuilder(); 1627// int start = 0; 1628// while (matcher.find()) { 1629// int matchStart = matcher.start(); 1630// int macthEnd = matcher.end(); 1631// if (start < matchStart) { 1632// if (toUpper) { 1633// buffer.append(content.substring(start, matchStart).toUpperCase()); 1634// } else { 1635// buffer.append(content.substring(start, matchStart).toLowerCase()); 1636// } 1637// } 1638// buffer.append(matcher.group().replaceAll(replace, "")); 1639// start = macthEnd; 1640// } 1641// if (start < content.length()) { 1642// if (toUpper) { 1643// buffer.append(content.substring(start).toUpperCase()); 1644// } else { 1645// buffer.append(content.substring(start).toLowerCase()); 1646// } 1647// } 1648// return buffer.toString(); 1649// } 1650 1651 public static boolean isTempTable(table table) { 1652 // Prefer structural classification: display names may retain identifier delimiters. 1653 if ("LOCAL_TEMP_TABLE".equals(table.getEndpointKind()) 1654 || "GLOBAL_TEMP_TABLE".equals(table.getEndpointKind()) 1655 || "TABLE_VARIABLE".equals(table.getEndpointKind())) { 1656 return true; 1657 } 1658 if(SubType.temp_table.name().equals(table.getSubType())) { 1659 return true; 1660 } 1661 String tableName = table.getName(); 1662 List<String> segments = parseNames(tableName); 1663 if (tableName.startsWith("@") || segments.get(segments.size() - 1).startsWith("@")) { 1664 return true; 1665 } 1666 if (tableName.startsWith("#") || segments.get(segments.size() - 1).startsWith("#")) { 1667 return true; 1668 } 1669 return false; 1670 } 1671 1672 /** 1673 * Whether a table's name alone makes it a local temporary table: {@code #name} in SQL 1674 * Server/Azure SQL and the SAP ASE/IQ/SQL Anywhere family, SAP HANA and Redshift, including 1675 * delimited names. A table made temporary by its declaration keywords ({@code CREATE TEMP 1676 * TABLE t}) is not recognizable from its name; see 1677 * {@link gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics}. 1678 */ 1679 public static boolean isLocalTemporaryTable(EDbVendor vendor, 1680 gudusoft.gsqlparser.nodes.TObjectName name) { 1681 return name != null && isLocalTemporaryTableName(vendor, name.getObjectString()); 1682 } 1683 1684 /** Same classification for a single SQL-lexical object segment (never a stored/display name). */ 1685 public static boolean isLocalTemporaryTableName(EDbVendor vendor, String lexicalObjectSegment) { 1686 return TemporaryTableSemantics.isTransient(TemporaryTableSemantics.ofNameSegment(vendor, lexicalObjectSegment)); 1687 } 1688 1689// public static boolean isTempTable(String tableName) { 1690// List<String> segments = parseNames(tableName); 1691// if (tableName.startsWith("@") || segments.get(segments.size() - 1).startsWith("@")) { 1692// return true; 1693// } 1694// if (tableName.startsWith("#") || segments.get(segments.size() - 1).startsWith("#")) { 1695// return true; 1696// } 1697// return false; 1698// } 1699 1700// public static boolean isTempTable(TTable table, EDbVendor vendor) { 1701// switch (vendor) { 1702// case dbvmssql: 1703// return table.getName().startsWith("#"); 1704// default: 1705// return false; 1706// } 1707// } 1708 1709 public static File[] listFiles(File sqlFiles) { 1710 List<File> children = new ArrayList<File>(); 1711 if (sqlFiles != null) 1712 listFiles(sqlFiles, children); 1713 Collections.sort(children); 1714 return children.toArray(new File[0]); 1715 } 1716 1717 public static File[] listFiles(File sqlFiles, FileFilter filter) { 1718 List<File> children = new ArrayList<File>(); 1719 if (sqlFiles != null) 1720 listFiles(sqlFiles, children, filter); 1721 Collections.sort(children); 1722 return children.toArray(new File[0]); 1723 } 1724 1725 private static Set<String> extensions = new HashSet<String>(Arrays.asList("java", "class", "php", "c", "cpp", "properties", "iml", 1726 "yml", "xml", "md", "jpg", "png", "gif", "bmp", "tiff", "tif", "svg", "pdf", "mp3", "mp4", "bak", "jar", "gz", 1727 "tar", "log", "conf", "dll", "exe", "so", "sh", "bat", "cdr", "docx", "doc", "xps", "xlsx", "xls", "ppt", "pptx", 1728 "rar", "7z", "ttf", "caj", "dwg", "dwf", "dxf", "ico", "epub", "webp", "heic", "html", "htm", "vsd", "vsdx", "rtf", "ico")); 1729 1730 public static void listFiles(File rootFile, List<File> children) { 1731 if (rootFile.isFile()) 1732 children.add(rootFile); 1733 else { 1734 File[] files = rootFile.listFiles(t -> { 1735 int dotIndex = t.getName().lastIndexOf("."); 1736 if (dotIndex == -1) { 1737 return true; 1738 } 1739 String extension = t.getName().toLowerCase(Locale.ROOT).substring(dotIndex + 1); 1740 return !extensions.contains(extension); 1741 }); 1742 if (files != null) { 1743 for (int i = 0; i < files.length; i++) { 1744 listFiles(files[i], children); 1745 } 1746 } 1747 } 1748 } 1749 1750 public static void listFiles(File rootFile, List<File> children, FileFilter filter) { 1751 if (rootFile.isFile() && filter.accept(rootFile)) 1752 children.add(rootFile); 1753 else { 1754 File[] files = rootFile.listFiles(filter); 1755 if (files != null) { 1756 for (int i = 0; i < files.length; i++) { 1757 listFiles(files[i], children, filter); 1758 } 1759 } 1760 } 1761 } 1762 1763 public static String readFile(File file) { 1764 StringBuilder stringBuilder = new StringBuilder(); 1765 BufferedReader reader = null; 1766 try { 1767 reader = new BufferedReader(new FileReader(file)); 1768 1769 String line = null; 1770 String ls = System.getProperty("line.separator"); 1771 while ((line = reader.readLine()) != null) { 1772 stringBuilder.append(line); 1773 stringBuilder.append(ls); 1774 } 1775 stringBuilder.deleteCharAt(stringBuilder.length() - 1); 1776 } catch (IOException e) { 1777 logger.error("read file failed.", e); 1778 } finally { 1779 if (reader != null) { 1780 try { 1781 reader.close(); 1782 } catch (IOException e) { 1783 logger.error("close reader failed.", e); 1784 } 1785 } 1786 } 1787 return stringBuilder.toString(); 1788 } 1789 1790 public static String stringToMD5(String plainText) { 1791 byte[] secretBytes = null; 1792 try { 1793 secretBytes = MessageDigest.getInstance("md5").digest(plainText.getBytes("UTF-8")); 1794 StringBuffer sb = new StringBuffer(); 1795 for (int i = 0; i < secretBytes.length; ++i) { 1796 sb.append(Integer.toHexString((secretBytes[i] & 0xFF) | 0x100).substring(1, 3)); 1797 } 1798 return sb.toString(); 1799 } catch (Exception e) { 1800 logger.error("get text md5 value failed.", e); 1801 } 1802 return null; 1803 } 1804 1805 public static String trimSingleQuote(String columnAlias) { 1806 if(columnAlias.startsWith("'") && columnAlias.endsWith("'")) { 1807 return columnAlias.substring(1, columnAlias.length() - 1); 1808 } 1809 return columnAlias; 1810 } 1811 1812 public static String endTrim(String input) { 1813 if (input == null) { 1814 return null; 1815 } 1816 1817 int end = input.length(); 1818 while (end > 0 && Character.isWhitespace(input.charAt(end - 1))) { 1819 end--; 1820 } 1821 1822 return input.substring(0, end); 1823 } 1824 1825 public static void endTrim(StringBuilder buffer) { 1826 int length = buffer.length(); 1827 while (length > 0 && Character.isWhitespace(buffer.charAt(length - 1))) { 1828 length--; // 逐步减少长度 1829 } 1830 buffer.setLength(length); // 直接截断尾部空白字符 1831 } 1832 1833 public static String joinNonEmpty(String... parts) { 1834 return Arrays.stream(parts) 1835 .filter(s -> s != null && !s.trim().isEmpty()) 1836 .collect(Collectors.joining("/")); 1837 } 1838 1839 public static long hash64(CharSequence cs) { 1840 long hash = 0xcbf29ce484222325L; 1841 for (int i = 0, len = cs.length(); i < len; i++) { 1842 hash ^= cs.charAt(i); 1843 hash *= 0x100000001b3L; 1844 } 1845 return hash; 1846 } 1847}