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