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