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