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}