001package gudusoft.gsqlparser.sqlenv;
002
003import gudusoft.gsqlparser.EDataType;
004import gudusoft.gsqlparser.EDbVendor;
005import gudusoft.gsqlparser.TBaseType;
006import gudusoft.gsqlparser.TCustomSqlStatement;
007import gudusoft.gsqlparser.TGSqlParser;
008import gudusoft.gsqlparser.nodes.TObjectName;
009import gudusoft.gsqlparser.nodes.TTypeName;
010import gudusoft.gsqlparser.sqlenv.catalog.CatalogStoreProvider;
011import gudusoft.gsqlparser.sqlenv.catalog.ICatalogProvider;
012import gudusoft.gsqlparser.util.Logger;
013import gudusoft.gsqlparser.util.LoggerFactory;
014import gudusoft.gsqlparser.util.SQLUtil;
015
016import java.util.ArrayList;
017import java.util.EnumMap;
018import java.util.List;
019import java.util.Locale;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.concurrent.CopyOnWriteArrayList;
023
024import static gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.*;
025
026/**
027 * SQL environment includes the metadata of a list of databases. The typical scenario is there is one database includes
028 * some schemas, and each schema includes some tables, views, procedures and etc.
029 * <br>
030 * <br>Database known as catalog in ANSI SQL.
031 * <br>Each catalog including a list of schemas.
032 * <br>Each schema including a list of schema objects such as table, procedure, function, trigger and more.
033 * <br>
034 * Implement your own concrete class derived from this class to get the metadata from a real database.
035 * Usually, this is done by querying the INFORMATION_SCHEMA in the {@link #initSQLEnv()} method which should be override
036 * in your own class.
037 * <br><br>
038 *
039 */
040public abstract class TSQLEnv {
041
042    private static final Logger logger = LoggerFactory.getLogger(TSQLEnv.class);
043
044    public static final Map<EDbVendor, Boolean> columnCollationCaseSensitive;
045    
046    static {
047        columnCollationCaseSensitive = new EnumMap<>(EDbVendor.class);
048        columnCollationCaseSensitive.put(EDbVendor.dbvaccess, false);
049        columnCollationCaseSensitive.put(EDbVendor.dbvansi, false);
050        columnCollationCaseSensitive.put(EDbVendor.dbvathena, false);
051        columnCollationCaseSensitive.put(EDbVendor.dbvazuresql, false);
052        columnCollationCaseSensitive.put(EDbVendor.dbvbigquery, false);
053        columnCollationCaseSensitive.put(EDbVendor.dbvclickhouse, false);
054        columnCollationCaseSensitive.put(EDbVendor.dbvcouchbase, false);
055        columnCollationCaseSensitive.put(EDbVendor.dbvdax, false);
056        columnCollationCaseSensitive.put(EDbVendor.dbvdb2, false);
057        columnCollationCaseSensitive.put(EDbVendor.dbvexasol, false);
058        columnCollationCaseSensitive.put(EDbVendor.dbvfirebird, false);
059        columnCollationCaseSensitive.put(EDbVendor.dbvgeneric, false);
060        columnCollationCaseSensitive.put(EDbVendor.dbvgreenplum, false);
061        columnCollationCaseSensitive.put(EDbVendor.dbvhana, false);
062        columnCollationCaseSensitive.put(EDbVendor.dbvhive, false);
063        columnCollationCaseSensitive.put(EDbVendor.dbvimpala, false);
064        columnCollationCaseSensitive.put(EDbVendor.dbvinformix, false);
065        columnCollationCaseSensitive.put(EDbVendor.dbvmdx, false);
066        columnCollationCaseSensitive.put(EDbVendor.dbvmysql, false);
067        columnCollationCaseSensitive.put(EDbVendor.dbvmssql, false);
068        columnCollationCaseSensitive.put(EDbVendor.dbvnetezza, false);
069        columnCollationCaseSensitive.put(EDbVendor.dbvodbc, false);
070        columnCollationCaseSensitive.put(EDbVendor.dbvopenedge, false);
071        columnCollationCaseSensitive.put(EDbVendor.dbvoracle, true);
072        columnCollationCaseSensitive.put(EDbVendor.dbvdameng, true);
073        columnCollationCaseSensitive.put(EDbVendor.dbvoceanbase, false);  // mirrors MySQL — default tenant mode is MYSQL
074        columnCollationCaseSensitive.put(EDbVendor.dbvpostgresql, false);
075        columnCollationCaseSensitive.put(EDbVendor.dbvduckdb, false);
076        columnCollationCaseSensitive.put(EDbVendor.dbvpresto, false);
077        columnCollationCaseSensitive.put(EDbVendor.dbvflink, false);
078        columnCollationCaseSensitive.put(EDbVendor.dbvredshift, false);
079        columnCollationCaseSensitive.put(EDbVendor.dbvsnowflake, false);
080        columnCollationCaseSensitive.put(EDbVendor.dbvsoql, false);
081        columnCollationCaseSensitive.put(EDbVendor.dbvsparksql, false);
082        columnCollationCaseSensitive.put(EDbVendor.dbvsybase, false);
083        columnCollationCaseSensitive.put(EDbVendor.dbvsybaseiq, false);  // mirrors dbvsybase (HC-9)
084        columnCollationCaseSensitive.put(EDbVendor.dbvsqlanywhere, false);  // mirrors dbvsybase (HC-9)
085        columnCollationCaseSensitive.put(EDbVendor.dbvsybasease, false);  // mirrors dbvsybase (HC-9)
086        columnCollationCaseSensitive.put(EDbVendor.dbvteradata, false);
087        columnCollationCaseSensitive.put(EDbVendor.dbvtrino, false);
088        columnCollationCaseSensitive.put(EDbVendor.dbvvertica, false);
089        columnCollationCaseSensitive.put(EDbVendor.dbvdatabricks, false);
090        columnCollationCaseSensitive.put(EDbVendor.dbvgaussdb, false);
091        columnCollationCaseSensitive.put(EDbVendor.dbvedb, false);
092        columnCollationCaseSensitive.put(EDbVendor.dbvdoris, false);  // Doris is MySQL-compatible, case-insensitive
093        columnCollationCaseSensitive.put(EDbVendor.dbvstarrocks, false);  // StarRocks is MySQL-compatible, case-insensitive
094        columnCollationCaseSensitive.put(EDbVendor.dbvsqlite, false);  // SQLite is case-insensitive for identifiers
095        columnCollationCaseSensitive.put(EDbVendor.dbvpowerquery, true);  // M identifiers are case-sensitive
096
097        // Compile-time check to ensure all enum values are covered
098        if (columnCollationCaseSensitive.size() != EDbVendor.values().length) {
099            throw new IllegalStateException("columnCollationCaseSensitive map must contain all EDbVendor values");
100        }
101    }
102
103    public static final Map<EDbVendor, Boolean> functionCollationCaseSensitive;
104    
105    static {
106        functionCollationCaseSensitive = new EnumMap<>(EDbVendor.class);
107        functionCollationCaseSensitive.put(EDbVendor.dbvaccess, false);
108        functionCollationCaseSensitive.put(EDbVendor.dbvansi, false);
109        functionCollationCaseSensitive.put(EDbVendor.dbvathena, false);
110        functionCollationCaseSensitive.put(EDbVendor.dbvazuresql, false);
111        functionCollationCaseSensitive.put(EDbVendor.dbvbigquery, false);
112        functionCollationCaseSensitive.put(EDbVendor.dbvclickhouse, false);
113        functionCollationCaseSensitive.put(EDbVendor.dbvcouchbase, false);
114        functionCollationCaseSensitive.put(EDbVendor.dbvdax, false);
115        functionCollationCaseSensitive.put(EDbVendor.dbvdb2, false);
116        functionCollationCaseSensitive.put(EDbVendor.dbvexasol, false);
117        functionCollationCaseSensitive.put(EDbVendor.dbvfirebird, false);
118        functionCollationCaseSensitive.put(EDbVendor.dbvgeneric, false);
119        functionCollationCaseSensitive.put(EDbVendor.dbvgreenplum, false);
120        functionCollationCaseSensitive.put(EDbVendor.dbvhana, false);
121        functionCollationCaseSensitive.put(EDbVendor.dbvhive, false);
122        functionCollationCaseSensitive.put(EDbVendor.dbvimpala, false);
123        functionCollationCaseSensitive.put(EDbVendor.dbvinformix, false);
124        functionCollationCaseSensitive.put(EDbVendor.dbvmdx, false);
125        functionCollationCaseSensitive.put(EDbVendor.dbvmysql, false);
126        functionCollationCaseSensitive.put(EDbVendor.dbvmssql, false);
127        functionCollationCaseSensitive.put(EDbVendor.dbvnetezza, false);
128        functionCollationCaseSensitive.put(EDbVendor.dbvodbc, false);
129        functionCollationCaseSensitive.put(EDbVendor.dbvopenedge, false);
130        functionCollationCaseSensitive.put(EDbVendor.dbvoracle, true);
131        functionCollationCaseSensitive.put(EDbVendor.dbvdameng, true);
132        functionCollationCaseSensitive.put(EDbVendor.dbvoceanbase, false);  // mirrors MySQL — default tenant mode is MYSQL
133        functionCollationCaseSensitive.put(EDbVendor.dbvpostgresql, true);
134        functionCollationCaseSensitive.put(EDbVendor.dbvduckdb, false);
135        functionCollationCaseSensitive.put(EDbVendor.dbvpresto, false);
136        functionCollationCaseSensitive.put(EDbVendor.dbvflink, false);
137        functionCollationCaseSensitive.put(EDbVendor.dbvredshift, false);
138        functionCollationCaseSensitive.put(EDbVendor.dbvsnowflake, false);
139        functionCollationCaseSensitive.put(EDbVendor.dbvsoql, false);
140        functionCollationCaseSensitive.put(EDbVendor.dbvsparksql, false);
141        functionCollationCaseSensitive.put(EDbVendor.dbvsybase, false);
142        functionCollationCaseSensitive.put(EDbVendor.dbvsybaseiq, false);  // mirrors dbvsybase (HC-9)
143        functionCollationCaseSensitive.put(EDbVendor.dbvsqlanywhere, false);  // mirrors dbvsybase (HC-9)
144        functionCollationCaseSensitive.put(EDbVendor.dbvsybasease, false);  // mirrors dbvsybase (HC-9)
145        functionCollationCaseSensitive.put(EDbVendor.dbvteradata, false);
146        functionCollationCaseSensitive.put(EDbVendor.dbvtrino, false);
147        functionCollationCaseSensitive.put(EDbVendor.dbvvertica, false);
148        functionCollationCaseSensitive.put(EDbVendor.dbvdatabricks, false);
149        functionCollationCaseSensitive.put(EDbVendor.dbvgaussdb, false);
150        functionCollationCaseSensitive.put(EDbVendor.dbvedb, false);
151        functionCollationCaseSensitive.put(EDbVendor.dbvdoris, false);  // Doris is MySQL-compatible, case-insensitive
152        functionCollationCaseSensitive.put(EDbVendor.dbvstarrocks, false);  // StarRocks is MySQL-compatible, case-insensitive
153        functionCollationCaseSensitive.put(EDbVendor.dbvsqlite, false);  // SQLite is case-insensitive for identifiers
154        functionCollationCaseSensitive.put(EDbVendor.dbvpowerquery, true);  // M is case-sensitive
155
156        // Compile-time check to ensure all enum values are covered
157        if (functionCollationCaseSensitive.size() != EDbVendor.values().length) {
158            throw new IllegalStateException("functionCollationCaseSensitive map must contain all EDbVendor values");
159        }
160    }
161    public static final Map<EDbVendor, Boolean> tableCollationCaseSensitive;
162    
163    static {
164        tableCollationCaseSensitive = new EnumMap<>(EDbVendor.class);
165        tableCollationCaseSensitive.put(EDbVendor.dbvaccess, false);
166        tableCollationCaseSensitive.put(EDbVendor.dbvansi, false);
167        tableCollationCaseSensitive.put(EDbVendor.dbvathena, false);
168        tableCollationCaseSensitive.put(EDbVendor.dbvazuresql, false);
169        tableCollationCaseSensitive.put(EDbVendor.dbvbigquery, false);
170        tableCollationCaseSensitive.put(EDbVendor.dbvclickhouse, false);
171        tableCollationCaseSensitive.put(EDbVendor.dbvcouchbase, false);
172        tableCollationCaseSensitive.put(EDbVendor.dbvdax, false);
173        tableCollationCaseSensitive.put(EDbVendor.dbvdb2, true);
174        tableCollationCaseSensitive.put(EDbVendor.dbvexasol, false);
175        tableCollationCaseSensitive.put(EDbVendor.dbvfirebird, false);
176        tableCollationCaseSensitive.put(EDbVendor.dbvgeneric, false);
177        tableCollationCaseSensitive.put(EDbVendor.dbvgreenplum, false);
178        tableCollationCaseSensitive.put(EDbVendor.dbvhana, false);
179        tableCollationCaseSensitive.put(EDbVendor.dbvhive, false);
180        tableCollationCaseSensitive.put(EDbVendor.dbvimpala, false);
181        tableCollationCaseSensitive.put(EDbVendor.dbvinformix, false);
182        tableCollationCaseSensitive.put(EDbVendor.dbvmdx, false);
183        tableCollationCaseSensitive.put(EDbVendor.dbvmysql, false);
184        tableCollationCaseSensitive.put(EDbVendor.dbvmssql, false);
185        tableCollationCaseSensitive.put(EDbVendor.dbvnetezza, false);
186        tableCollationCaseSensitive.put(EDbVendor.dbvodbc, false);
187        tableCollationCaseSensitive.put(EDbVendor.dbvopenedge, false);
188        tableCollationCaseSensitive.put(EDbVendor.dbvoracle, true);
189        tableCollationCaseSensitive.put(EDbVendor.dbvdameng, true);
190        tableCollationCaseSensitive.put(EDbVendor.dbvoceanbase, false);  // mirrors MySQL — default tenant mode is MYSQL
191        tableCollationCaseSensitive.put(EDbVendor.dbvpostgresql, true);
192        tableCollationCaseSensitive.put(EDbVendor.dbvduckdb, false);
193        tableCollationCaseSensitive.put(EDbVendor.dbvpresto, false);
194        tableCollationCaseSensitive.put(EDbVendor.dbvflink, false);
195        tableCollationCaseSensitive.put(EDbVendor.dbvredshift, false);
196        tableCollationCaseSensitive.put(EDbVendor.dbvsnowflake, false);
197        tableCollationCaseSensitive.put(EDbVendor.dbvsoql, false);
198        tableCollationCaseSensitive.put(EDbVendor.dbvsparksql, false);
199        tableCollationCaseSensitive.put(EDbVendor.dbvsybase, false);
200        tableCollationCaseSensitive.put(EDbVendor.dbvsybaseiq, false);  // mirrors dbvsybase (HC-9)
201        tableCollationCaseSensitive.put(EDbVendor.dbvsqlanywhere, false);  // mirrors dbvsybase (HC-9)
202        tableCollationCaseSensitive.put(EDbVendor.dbvsybasease, false);  // mirrors dbvsybase (HC-9)
203        tableCollationCaseSensitive.put(EDbVendor.dbvteradata, false);
204        tableCollationCaseSensitive.put(EDbVendor.dbvtrino, false);
205        tableCollationCaseSensitive.put(EDbVendor.dbvvertica, false);
206        tableCollationCaseSensitive.put(EDbVendor.dbvdatabricks, false);
207        tableCollationCaseSensitive.put(EDbVendor.dbvgaussdb, false);
208        tableCollationCaseSensitive.put(EDbVendor.dbvedb, false);
209        tableCollationCaseSensitive.put(EDbVendor.dbvdoris, false);  // Doris is MySQL-compatible, case-insensitive
210        tableCollationCaseSensitive.put(EDbVendor.dbvstarrocks, false);  // StarRocks is MySQL-compatible, case-insensitive
211        tableCollationCaseSensitive.put(EDbVendor.dbvsqlite, false);  // SQLite is case-insensitive for identifiers
212        tableCollationCaseSensitive.put(EDbVendor.dbvpowerquery, true);  // M is case-sensitive
213
214        // Compile-time check to ensure all enum values are covered
215        if (tableCollationCaseSensitive.size() != EDbVendor.values().length) {
216            throw new IllegalStateException("tableCollationCaseSensitive map must contain all EDbVendor values");
217        }
218    }
219
220    public static final Map<EDbVendor, Boolean> catalogCollationCaseSensitive;
221    
222    static {
223        catalogCollationCaseSensitive = new EnumMap<>(EDbVendor.class);
224        catalogCollationCaseSensitive.put(EDbVendor.dbvaccess, false);
225        catalogCollationCaseSensitive.put(EDbVendor.dbvansi, false);
226        catalogCollationCaseSensitive.put(EDbVendor.dbvathena, false);
227        catalogCollationCaseSensitive.put(EDbVendor.dbvazuresql, false);
228        catalogCollationCaseSensitive.put(EDbVendor.dbvbigquery, false);
229        catalogCollationCaseSensitive.put(EDbVendor.dbvclickhouse, false);
230        catalogCollationCaseSensitive.put(EDbVendor.dbvcouchbase, false);
231        catalogCollationCaseSensitive.put(EDbVendor.dbvdax, false);
232        catalogCollationCaseSensitive.put(EDbVendor.dbvdb2, true);
233        catalogCollationCaseSensitive.put(EDbVendor.dbvexasol, false);
234        catalogCollationCaseSensitive.put(EDbVendor.dbvfirebird, false);
235        catalogCollationCaseSensitive.put(EDbVendor.dbvgeneric, false);
236        catalogCollationCaseSensitive.put(EDbVendor.dbvgreenplum, false);
237        catalogCollationCaseSensitive.put(EDbVendor.dbvhana, false);
238        catalogCollationCaseSensitive.put(EDbVendor.dbvhive, false);
239        catalogCollationCaseSensitive.put(EDbVendor.dbvimpala, false);
240        catalogCollationCaseSensitive.put(EDbVendor.dbvinformix, false);
241        catalogCollationCaseSensitive.put(EDbVendor.dbvmdx, false);
242        catalogCollationCaseSensitive.put(EDbVendor.dbvmysql, true);
243        catalogCollationCaseSensitive.put(EDbVendor.dbvmssql, false);
244        catalogCollationCaseSensitive.put(EDbVendor.dbvnetezza, false);
245        catalogCollationCaseSensitive.put(EDbVendor.dbvodbc, false);
246        catalogCollationCaseSensitive.put(EDbVendor.dbvopenedge, false);
247        catalogCollationCaseSensitive.put(EDbVendor.dbvoracle, false);
248        catalogCollationCaseSensitive.put(EDbVendor.dbvdameng, false);
249        catalogCollationCaseSensitive.put(EDbVendor.dbvoceanbase, true);  // mirrors MySQL — catalog (database) is case-sensitive
250        catalogCollationCaseSensitive.put(EDbVendor.dbvpostgresql, true);
251        catalogCollationCaseSensitive.put(EDbVendor.dbvduckdb, false);
252        catalogCollationCaseSensitive.put(EDbVendor.dbvpresto, false);
253        catalogCollationCaseSensitive.put(EDbVendor.dbvflink, false);
254        catalogCollationCaseSensitive.put(EDbVendor.dbvredshift, false);
255        catalogCollationCaseSensitive.put(EDbVendor.dbvsnowflake, false);
256        catalogCollationCaseSensitive.put(EDbVendor.dbvsoql, false);
257        catalogCollationCaseSensitive.put(EDbVendor.dbvsparksql, false);
258        catalogCollationCaseSensitive.put(EDbVendor.dbvsybase, false);
259        catalogCollationCaseSensitive.put(EDbVendor.dbvsybaseiq, false);  // mirrors dbvsybase (HC-9)
260        catalogCollationCaseSensitive.put(EDbVendor.dbvsqlanywhere, false);  // mirrors dbvsybase (HC-9)
261        catalogCollationCaseSensitive.put(EDbVendor.dbvsybasease, false);  // mirrors dbvsybase (HC-9)
262        catalogCollationCaseSensitive.put(EDbVendor.dbvteradata, false);
263        catalogCollationCaseSensitive.put(EDbVendor.dbvtrino, false);
264        catalogCollationCaseSensitive.put(EDbVendor.dbvvertica, false);
265        catalogCollationCaseSensitive.put(EDbVendor.dbvdatabricks, false);
266        catalogCollationCaseSensitive.put(EDbVendor.dbvgaussdb, false);
267        catalogCollationCaseSensitive.put(EDbVendor.dbvedb, false);
268        catalogCollationCaseSensitive.put(EDbVendor.dbvdoris, true);  // Doris is MySQL-compatible, catalog (database) is case-sensitive
269        catalogCollationCaseSensitive.put(EDbVendor.dbvstarrocks, true);  // StarRocks is MySQL-compatible, catalog (database) is case-sensitive
270        catalogCollationCaseSensitive.put(EDbVendor.dbvsqlite, true);  // SQLite database names (file paths) are case-sensitive on most OS
271        catalogCollationCaseSensitive.put(EDbVendor.dbvpowerquery, true);  // M catalog references are case-sensitive
272
273        // Compile-time check to ensure all enum values are covered
274        if (catalogCollationCaseSensitive.size() != EDbVendor.values().length) {
275            throw new IllegalStateException("catalogCollationCaseSensitive map must contain all EDbVendor values");
276        }
277    }
278
279    public static final Map<EDbVendor, Boolean> defaultCollationCaseSensitive;
280    
281    static {
282        defaultCollationCaseSensitive = new EnumMap<>(EDbVendor.class);
283        defaultCollationCaseSensitive.put(EDbVendor.dbvaccess, false);
284        defaultCollationCaseSensitive.put(EDbVendor.dbvansi, false);
285        defaultCollationCaseSensitive.put(EDbVendor.dbvathena, false);
286        defaultCollationCaseSensitive.put(EDbVendor.dbvazuresql, false);
287        defaultCollationCaseSensitive.put(EDbVendor.dbvbigquery, false);
288        defaultCollationCaseSensitive.put(EDbVendor.dbvclickhouse, false);
289        defaultCollationCaseSensitive.put(EDbVendor.dbvcouchbase, false);
290        defaultCollationCaseSensitive.put(EDbVendor.dbvdax, false);
291        defaultCollationCaseSensitive.put(EDbVendor.dbvdb2, false);
292        defaultCollationCaseSensitive.put(EDbVendor.dbvexasol, false);
293        defaultCollationCaseSensitive.put(EDbVendor.dbvfirebird, false);
294        defaultCollationCaseSensitive.put(EDbVendor.dbvgeneric, false);
295        defaultCollationCaseSensitive.put(EDbVendor.dbvgreenplum, false);
296        defaultCollationCaseSensitive.put(EDbVendor.dbvhana, false);
297        defaultCollationCaseSensitive.put(EDbVendor.dbvhive, false);
298        defaultCollationCaseSensitive.put(EDbVendor.dbvimpala, false);
299        defaultCollationCaseSensitive.put(EDbVendor.dbvinformix, false);
300        defaultCollationCaseSensitive.put(EDbVendor.dbvmdx, false);
301        defaultCollationCaseSensitive.put(EDbVendor.dbvmysql, false);
302        defaultCollationCaseSensitive.put(EDbVendor.dbvmssql, false);
303        defaultCollationCaseSensitive.put(EDbVendor.dbvnetezza, false);
304        defaultCollationCaseSensitive.put(EDbVendor.dbvodbc, false);
305        defaultCollationCaseSensitive.put(EDbVendor.dbvopenedge, false);
306        defaultCollationCaseSensitive.put(EDbVendor.dbvoracle, false);
307        defaultCollationCaseSensitive.put(EDbVendor.dbvdameng, false);
308        defaultCollationCaseSensitive.put(EDbVendor.dbvoceanbase, false);  // mirrors MySQL — default tenant mode is MYSQL
309        defaultCollationCaseSensitive.put(EDbVendor.dbvpostgresql, false);
310        defaultCollationCaseSensitive.put(EDbVendor.dbvduckdb, false);
311        defaultCollationCaseSensitive.put(EDbVendor.dbvpresto, false);
312        defaultCollationCaseSensitive.put(EDbVendor.dbvflink, false);
313        defaultCollationCaseSensitive.put(EDbVendor.dbvredshift, false);
314        defaultCollationCaseSensitive.put(EDbVendor.dbvsnowflake, false);
315        defaultCollationCaseSensitive.put(EDbVendor.dbvsoql, false);
316        defaultCollationCaseSensitive.put(EDbVendor.dbvsparksql, false);
317        defaultCollationCaseSensitive.put(EDbVendor.dbvsybase, false);
318        defaultCollationCaseSensitive.put(EDbVendor.dbvsybaseiq, false);  // mirrors dbvsybase (HC-9)
319        defaultCollationCaseSensitive.put(EDbVendor.dbvsqlanywhere, false);  // mirrors dbvsybase (HC-9)
320        defaultCollationCaseSensitive.put(EDbVendor.dbvsybasease, false);  // mirrors dbvsybase (HC-9)
321        defaultCollationCaseSensitive.put(EDbVendor.dbvteradata, false);
322        defaultCollationCaseSensitive.put(EDbVendor.dbvtrino, false);
323        defaultCollationCaseSensitive.put(EDbVendor.dbvvertica, false);
324        defaultCollationCaseSensitive.put(EDbVendor.dbvdatabricks, false);
325        defaultCollationCaseSensitive.put(EDbVendor.dbvgaussdb, false);
326        defaultCollationCaseSensitive.put(EDbVendor.dbvedb, false);
327        defaultCollationCaseSensitive.put(EDbVendor.dbvdoris, false);  // Doris is MySQL-compatible, case-insensitive
328        defaultCollationCaseSensitive.put(EDbVendor.dbvstarrocks, false);  // StarRocks is MySQL-compatible, case-insensitive
329        defaultCollationCaseSensitive.put(EDbVendor.dbvsqlite, false);  // SQLite is case-insensitive by default
330        defaultCollationCaseSensitive.put(EDbVendor.dbvpowerquery, true);  // M is case-sensitive by default
331
332        // Compile-time check to ensure all enum values are covered
333        if (defaultCollationCaseSensitive.size() != EDbVendor.values().length) {
334            throw new IllegalStateException("defaultCollationCaseSensitive map must contain all EDbVendor values");
335        }
336    }
337
338
339
340    public static final Map<EDbVendor, Boolean> isAliasReferenceForbidden;
341
342    /**
343     * SAP IQ and SQL Anywhere, and {@code dbvsybase}, which follows them where ASE differs: "Aliases can be used
344     * anywhere in the SELECT block in which they are defined, including other SELECT list expressions", and "if the
345     * alias specified for an expression is identical to the name of a column or variable in the name space of the
346     * SELECT block, the alias definition occludes the column or variable" (SQL Anywhere, <i>Renamed columns in query
347     * results</i>; SAP IQ, <i>SELECT statement</i>: "Alias names can be used throughout the query"). So an unqualified
348     * name that matches an alias of its own SELECT block is that alias, even when a column of the same name exists;
349     * only a table-qualified name reaches the column. From a subquery that provably has no such column, the name is the
350     * alias of an enclosing block ({@code TSelectSqlStatement.findEnclosingAliasDefinition}) (Mantis 4752). Sybase ASE
351     * ({@code dbvsybasease}) has no such rule.
352     *
353     * <p>The single definition of that vendor rule for the older resolver, resolver2 and lineage.</p>
354     */
355    public static boolean isAliasOccludingColumn(EDbVendor vendor) {
356        return vendor == EDbVendor.dbvsybase
357                || vendor == EDbVendor.dbvsybaseiq
358                || vendor == EDbVendor.dbvsqlanywhere;
359    }
360
361    /**
362     * Dialects whose {@link #isAliasReferenceForbidden} entry is {@code true}, yet whose HAVING clause can name a
363     * SELECT-list alias of its own SELECT block, so {@code SELECT o.amount AS amt FROM orders o GROUP BY o.amount
364     * HAVING amt > 1} reads the alias:
365     * <ul>
366     *   <li>MySQL, and OceanBase in a MySQL tenant: "MySQL ... permits HAVING to refer to columns in the SELECT list"
367     *   (Reference Manual, <i>SELECT Statement</i>); see {@link #isHavingAliasOutrankingUngroupedColumn};</li>
368     *   <li>BigQuery: "The HAVING clause can reference columns available via the FROM clause, as well as SELECT list
369     *   aliases" (GoogleSQL <i>Query syntax</i>, HAVING clause); a name that is both, naming different objects, is an
370     *   error;</li>
371     *   <li>Spark SQL and Databricks: {@code SELECT city, sum(quantity) AS sum FROM dealer GROUP BY city HAVING sum >
372     *   15} (<i>HAVING clause</i>, "referring to aggregate function by its alias");</li>
373     *   <li>Hive, which Spark SQL follows here;</li>
374     *   <li>Impala ({@code HAVING} on an aggregate's alias in the <i>HAVING Clause</i> examples) and Doris, which
375     *   permits a column alias in HAVING (<i>SELECT</i>).</li>
376     * </ul>
377     * StarRocks is not listed: it takes an output alias there only when {@code enable_groupby_use_output_alias} is
378     * set, which is off by default. Snowflake, Vertica, Teradata and the dialects of {@link #isAliasOccludingColumn}
379     * expose the alias in HAVING already, through the resolvers' own rule. Every other dialect, PostgreSQL, Oracle and
380     * SQL Server among them, reads a HAVING name as a column. The single definition of that rule for the older
381     * resolver, resolver2 and lineage.
382     *
383     * @param vendor the SQL's vendor
384     * @param parser the parser that produced it, for the OceanBase tenant mode; null reads as a MySQL tenant
385     */
386    public static boolean isAliasVisibleInHaving(EDbVendor vendor, TGSqlParser parser) {
387        if (vendor == null) return false;
388        switch (vendor) {
389            case dbvbigquery:
390            case dbvsparksql:
391            case dbvdatabricks:
392            case dbvhive:
393            case dbvimpala:
394            case dbvdoris:
395                return true;
396            default:
397                return TCustomSqlStatement.isMySQLDialect(vendor, parser);
398        }
399    }
400
401    /**
402     * MySQL, and OceanBase in a MySQL tenant: a HAVING name that is both a SELECT-list alias and a column of the FROM
403     * clause is the column only when GROUP BY names that column; otherwise it is the alias. "If a HAVING column name is
404     * used both in GROUP BY and as an aliased column in the select column list, preference is given to the column in
405     * the GROUP BY column" (Reference Manual, <i>SELECT Statement</i>). In the other dialects of
406     * {@link #isAliasVisibleInHaving} a column the FROM clause provably has keeps the name.
407     */
408    public static boolean isHavingAliasOutrankingUngroupedColumn(EDbVendor vendor, TGSqlParser parser) {
409        return TCustomSqlStatement.isMySQLDialect(vendor, parser);
410    }
411
412    static {
413        isAliasReferenceForbidden = new EnumMap<>(EDbVendor.class);
414        isAliasReferenceForbidden.put(EDbVendor.dbvaccess, false);
415        isAliasReferenceForbidden.put(EDbVendor.dbvansi, false);
416        isAliasReferenceForbidden.put(EDbVendor.dbvathena, false);
417        isAliasReferenceForbidden.put(EDbVendor.dbvazuresql, true);
418        isAliasReferenceForbidden.put(EDbVendor.dbvbigquery, true);
419        isAliasReferenceForbidden.put(EDbVendor.dbvclickhouse, false);
420        isAliasReferenceForbidden.put(EDbVendor.dbvcouchbase, false);
421        isAliasReferenceForbidden.put(EDbVendor.dbvdax, false);
422        isAliasReferenceForbidden.put(EDbVendor.dbvdb2, true);
423        isAliasReferenceForbidden.put(EDbVendor.dbvexasol, false);  // Exasol disallows forward references
424        isAliasReferenceForbidden.put(EDbVendor.dbvfirebird, false);  // Firebird follows standard SQL behavior
425        isAliasReferenceForbidden.put(EDbVendor.dbvgeneric, false);
426        isAliasReferenceForbidden.put(EDbVendor.dbvgreenplum, true);
427        isAliasReferenceForbidden.put(EDbVendor.dbvhana, false);  // SAP HANA follows standard SQL behavior
428        isAliasReferenceForbidden.put(EDbVendor.dbvhive, true);
429        isAliasReferenceForbidden.put(EDbVendor.dbvimpala, false);  // Impala follows Hive's behavior
430        isAliasReferenceForbidden.put(EDbVendor.dbvinformix, true);
431        isAliasReferenceForbidden.put(EDbVendor.dbvmdx, false);
432        isAliasReferenceForbidden.put(EDbVendor.dbvmysql, true);
433        isAliasReferenceForbidden.put(EDbVendor.dbvmssql, true);
434        isAliasReferenceForbidden.put(EDbVendor.dbvnetezza, true);
435        isAliasReferenceForbidden.put(EDbVendor.dbvodbc, false);
436        isAliasReferenceForbidden.put(EDbVendor.dbvopenedge, false);
437        isAliasReferenceForbidden.put(EDbVendor.dbvoracle, true);
438        isAliasReferenceForbidden.put(EDbVendor.dbvdameng, true);
439        isAliasReferenceForbidden.put(EDbVendor.dbvoceanbase, true);  // mirrors MySQL — alias forward reference forbidden
440        isAliasReferenceForbidden.put(EDbVendor.dbvpostgresql, true);
441        isAliasReferenceForbidden.put(EDbVendor.dbvduckdb, true);
442        isAliasReferenceForbidden.put(EDbVendor.dbvpresto, true);
443        isAliasReferenceForbidden.put(EDbVendor.dbvflink, true);  // Flink follows standard SQL behavior like SparkSQL
444        isAliasReferenceForbidden.put(EDbVendor.dbvredshift, true);
445        isAliasReferenceForbidden.put(EDbVendor.dbvsnowflake, false);  // Snowflake disallows column alias references
446        isAliasReferenceForbidden.put(EDbVendor.dbvsoql, false);
447        isAliasReferenceForbidden.put(EDbVendor.dbvsparksql, true);
448        // SAP IQ and SQL Anywhere: an alias can be used anywhere in the SELECT block that defines it, and it
449        // occludes a column of the same name (Mantis 4752). dbvsybase follows them where ASE differs.
450        isAliasReferenceForbidden.put(EDbVendor.dbvsybase, false);
451        isAliasReferenceForbidden.put(EDbVendor.dbvsybaseiq, false);
452        isAliasReferenceForbidden.put(EDbVendor.dbvsqlanywhere, false);
453        isAliasReferenceForbidden.put(EDbVendor.dbvsybasease, true);  // ASE (Transact-SQL): no alias reference outside ORDER BY
454        isAliasReferenceForbidden.put(EDbVendor.dbvteradata, false);  // Teradata follows standard SQL behavior
455        isAliasReferenceForbidden.put(EDbVendor.dbvtrino, true);
456        isAliasReferenceForbidden.put(EDbVendor.dbvvertica, false);  // Vertica disallows forward references
457        isAliasReferenceForbidden.put(EDbVendor.dbvdatabricks, true);
458        isAliasReferenceForbidden.put(EDbVendor.dbvgaussdb, true);
459        isAliasReferenceForbidden.put(EDbVendor.dbvedb, true);
460        isAliasReferenceForbidden.put(EDbVendor.dbvdoris, true);  // Doris is MySQL-compatible, alias reference forbidden
461        isAliasReferenceForbidden.put(EDbVendor.dbvstarrocks, true);  // StarRocks is MySQL-compatible, alias reference forbidden
462        isAliasReferenceForbidden.put(EDbVendor.dbvsqlite, true);  // SQLite follows PostgreSQL behavior, alias reference forbidden in same clause
463        isAliasReferenceForbidden.put(EDbVendor.dbvpowerquery, true);  // M step-to-step references are forbidden before binding
464
465        // Compile-time check to ensure all enum values are covered
466        if (isAliasReferenceForbidden.size() != EDbVendor.values().length) {
467            throw new IllegalStateException("isAliasReferenceForbidden map must contain all EDbVendor values");
468        }
469    }
470
471    public static String getStmtSeparatorChar(EDbVendor dbVendor){
472        String ret = ";";
473        switch (dbVendor){
474            case dbvoracle:
475            case dbvteradata:
476            case dbvpostgresql:
477            case dbvduckdb:
478            case dbvredshift:
479            case dbvgreenplum:
480                ret = "/";
481                break;
482            case dbvdameng:
483                ret = "/";
484                break;
485            case dbvdb2:
486                ret = "@";
487                break;
488            case dbvmysql:
489                ret = "$";
490                break;
491            default:
492                break;
493        }
494        return ret;
495    }
496
497
498    /**
499     * Whether this database support schema or not?
500     *
501     * @param dbVendor
502     * @return
503     */
504    public static boolean supportSchema(EDbVendor dbVendor){
505        return (!((dbVendor == EDbVendor.dbvmysql)
506                ||(dbVendor == EDbVendor.dbvteradata)||(dbVendor == EDbVendor.dbvhive)||(dbVendor == EDbVendor.dbvimpala)
507                ));
508    }
509
510    /**
511     * Whether this database support catalog or not?
512     *
513     * @param dbVendor
514     * @return
515     */
516    public static boolean supportCatalog(EDbVendor dbVendor) {
517//        return (!((dbVendor == EDbVendor.dbvoracle)
518//        ));
519        return true;
520    }
521
522    /**
523     * used to delimit a database identifier, such as [ used in SQL Server, ` used in MySQL
524     *
525     * @param dbVendor
526     * @return
527     */
528    public static String delimitedChar(EDbVendor dbVendor){
529        String ret = "\"";
530        switch (dbVendor){
531            case dbvmssql:
532            case dbvazuresql:
533                ret = "[";
534                break;
535            case dbvathena:
536            case dbvmysql:
537            case dbvbigquery:
538            case dbvcouchbase:
539            case dbvhive:
540            case dbvimpala:
541            case dbvdatabricks:
542                ret = "`";
543                break;
544            case dbvdax:
545                ret = "'";
546                break;
547            default:
548                break;
549        }
550        return ret;
551    }
552
553    public static boolean isDelimitedIdentifier(EDbVendor dbVendor, String identifier){
554        boolean ret = false;
555        switch (dbVendor){
556            case dbvmssql:
557            case dbvazuresql:
558                ret = identifier.startsWith("[")||identifier.startsWith("\"")||identifier.startsWith("'");
559                break;
560            case dbvmysql:
561            case dbvbigquery:
562            case dbvcouchbase:
563            case dbvhive:
564            case dbvimpala:
565                ret = identifier.startsWith("`");
566                break;
567            case dbvdax:
568                ret = identifier.startsWith("'");
569                break;
570            default:
571                ret = identifier.startsWith("\"");
572                break;
573        }
574        return ret;
575    }
576    
577    public static boolean endsWithDelimitedIdentifier(EDbVendor dbVendor, String identifier){
578        boolean ret = false;
579        switch (dbVendor){
580            case dbvmssql:
581            case dbvazuresql:
582                ret = identifier.endsWith("]")||identifier.endsWith("\"");
583                break;
584            case dbvmysql:
585            case dbvbigquery:
586            case dbvcouchbase:
587            case dbvhive:
588            case dbvimpala:
589                ret = identifier.endsWith("`");
590                break;
591            case dbvdax:
592                ret = identifier.endsWith("'");
593                break;
594            default:
595                ret = identifier.endsWith("\"");
596                break;
597        }
598        return ret;
599    }
600
601    public boolean isDelimitedIdentifier(String identifier){
602        return TSQLEnv.isDelimitedIdentifier(this.getDBVendor(),identifier);
603    }
604
605
606    public boolean compareColumn(String ident1, String ident2){
607        // Route through NameService when flag is enabled
608        if (TBaseType.USE_EXT_NAME_MATCHER) {
609            return nameService.equals(ESQLDataObjectType.dotColumn, ident1, ident2);
610        }
611        return compareIdentifier(this.getDBVendor(),ESQLDataObjectType.dotColumn,ident1,ident2);
612    }
613
614    public boolean compareTable(String ident1, String ident2){
615        // Route through NameService when flag is enabled
616        if (TBaseType.USE_EXT_NAME_MATCHER) {
617            return nameService.equals(ESQLDataObjectType.dotTable, ident1, ident2);
618        }
619        return compareIdentifier(this.getDBVendor(),ESQLDataObjectType.dotTable,ident1,ident2);
620    }
621
622    public boolean compareIdentifier(ESQLDataObjectType objectType, String ident1, String ident2){
623        // Route through NameService when flag is enabled
624        if (TBaseType.USE_EXT_NAME_MATCHER) {
625            return nameService.equals(objectType, ident1, ident2);
626        }
627
628        // Legacy path: use existing logic
629        switch (objectType){
630            case dotTable:
631                return compareTable(ident1,ident2);
632            case dotColumn:
633                return compareColumn(ident1,ident2);
634            default:
635                return compareIdentifier(this.getDBVendor(),objectType,ident1,ident2);
636        }
637    }
638
639    public static boolean compareColumn( EDbVendor dbVendor, TObjectName sourceColumn, TObjectName targetColumn){
640        return compareQualifiedColumn(dbVendor,sourceColumn.getColumnNameOnly(),targetColumn.getColumnNameOnly(),
641                sourceColumn.getTableString(),targetColumn.getTableString(),
642                sourceColumn.getSchemaString(),targetColumn.getSchemaString(),
643                sourceColumn.getDatabaseString(),targetColumn.getDatabaseString());
644    }
645
646    public static boolean compareTable( EDbVendor dbVendor, TObjectName sourceTable, TObjectName targetTable){
647        return compareQualifiedTable(dbVendor,
648                sourceTable.getTableString(),targetTable.getTableString(),
649                sourceTable.getSchemaString(),targetTable.getSchemaString(),
650                sourceTable.getDatabaseString(),targetTable.getDatabaseString()
651        );
652    }
653
654    public static boolean compareQualifiedTable( EDbVendor dbVendor, String sourceTable, String targetTable, String sourceSchema, String targetSchema, String sourceDatabase, String targetDatabase){
655        boolean ret = compareIdentifier(dbVendor,dotTable,sourceTable,targetTable);
656        if (!ret) return ret;
657
658        // compare schema
659        ret = compareIdentifier(dbVendor,dotSchema,sourceSchema,targetSchema);
660        if (!ret) return ret;
661
662        // compare database
663        return compareIdentifier(dbVendor,dotCatalog,sourceDatabase,targetDatabase);
664    }
665
666    public static boolean compareQualifiedColumn( EDbVendor dbVendor, String sourceColumn, String targetColumn, String sourceTable, String targetTable, String sourceSchema, String targetSchema, String sourceDatabase, String targetDatabase){
667
668        boolean ret = compareIdentifier(dbVendor,dotColumn,sourceColumn,targetColumn);
669        if (!ret) return ret;
670
671        return compareQualifiedTable(dbVendor,sourceTable,targetTable,sourceSchema,targetSchema,sourceDatabase,targetDatabase);
672    }
673
674    /**
675     * @deprecated Compares the {@code toString()} of the two object names, which can be a
676     * multi-segment qualified string ({@code sch.t.c}) fed into a single-segment compare.
677     * Use {@link gudusoft.gsqlparser.util.SQLUtil#compareIdentifier(EDbVendor,
678     * ESQLDataObjectType, String, String)} for possibly-qualified names, or
679     * {@link gudusoft.gsqlparser.util.SQLUtil#sameName(EDbVendor, ESQLDataObjectType,
680     * String, String)} on the raw single-segment parts. See
681     * {@code gsp_java_core/doc/user_guide/identifier_normalization_guide.md}.
682     */
683    @Deprecated
684    public static boolean compareIdentifier( EDbVendor dbVendor, ESQLDataObjectType objectType, TObjectName source, TObjectName target){
685        return compareIdentifier(dbVendor,objectType,source.toString(),target.toString());
686    }
687
688    /**
689     * Single-segment identifier equality — the same canonical relation as
690     * {@link gudusoft.gsqlparser.util.SQLUtil#sameName(EDbVendor, ESQLDataObjectType,
691     * String, String)}, which delegates here. New code should call the {@code SQLUtil}
692     * façade instead; this entry point remains for source compatibility. Both operands
693     * must be single name segments — for {@code a.b.c} shapes use
694     * {@link gudusoft.gsqlparser.util.SQLUtil#compareIdentifier(EDbVendor,
695     * ESQLDataObjectType, String, String)}.
696     */
697    public static boolean compareIdentifier( EDbVendor dbVendor, ESQLDataObjectType objectType, String ident1, String ident2){
698        // P0d.2: delegated to the canonical engine — ONE identifier-equality relation
699        // (canonical-key equality, see IdentifierService.areEqual / CanonKey). The previous
700        // normalize-then-per-objectType-boolean policy collapsed quoted-case distinctions on
701        // vendors whose profile preserves them (PostgreSQL columns, Snowflake, and the
702        // synonyms bucket) — the P0c eligibility report records those cells as the expected
703        // behavior deltas of this change.
704        return IdentifierService.areEqualStatic(dbVendor, objectType, ident1, ident2);
705    }
706
707    /**
708     * The spelling to hand to {@code createTable} / {@code addColumn} / {@code createSchema} and
709     * the other catalog entry points, for a name that arrived in the given input form.
710     *
711     * <p>Those entry points treat every name as SQL text: delimiters are recognized and an
712     * undelimited name is case-folded by the vendor's rule. That is right for a name read from
713     * SQL, and wrong for a name read back from a database catalog. Oracle stores the column
714     * created as {@code "Amount"} under the name {@code Amount}; registered as-is it was folded to
715     * {@code AMOUNT}, so the only legal reference {@code "Amount"} resolved to NOT_FOUND while
716     * {@code amount}, which Oracle rejects, resolved to EXACT_MATCH.</p>
717     *
718     * <p>A {@link IdentifierInputForm#CATALOG_STORED} name is therefore converted here, at the
719     * door, into the always-delimited SQL spelling of the same stored name
720     * ({@link IdentifierCodec#encodeStored}). That is exactly the form the sqlflow-ingester
721     * exporter already emits ({@code '"'||column_name||'"'}), so nothing downstream needs a second
722     * code path. A {@link IdentifierInputForm#SQL_LEXICAL} name is returned unchanged.</p>
723     *
724     * <p>The form is a property of the INPUT, declared once per metadata file ({@code nameForm});
725     * a loader must not guess it from how a string looks. {@code [db name]} is a stored name with
726     * brackets in it or the SQL spelling of {@code db name}, and the text cannot say which.</p>
727     *
728     * @param vendor dialect whose delimiter and escaping rules apply
729     * @param objectType role of the name (delimiter rules can depend on it)
730     * @param name the name as it arrived; null or empty is returned as is
731     * @param form how the name arrived; null means {@link IdentifierInputForm#SQL_LEXICAL}
732     */
733    public static String toLexical(EDbVendor vendor, ESQLDataObjectType objectType, String name,
734                                   IdentifierInputForm form) {
735        if (name == null || name.isEmpty() || form != IdentifierInputForm.CATALOG_STORED
736                || vendor == null || objectType == null) {
737            return name;
738        }
739        // non-identifier-compare: DEFAULT / DEFAULT_SERVER are this library's placeholders for a
740        // tier the dialect or the file does not have, not names read from a catalog.
741        if ((objectType == ESQLDataObjectType.dotCatalog && DEFAULT_DB_NAME.equals(name))
742                || (objectType == ESQLDataObjectType.dotSchema && DEFAULT_SCHEMA_NAME.equals(name))) {
743            return name;
744        }
745        IdentifierProfile profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults());
746        if (!IdentifierCodec.hasQuotedForm(profile, objectType)) {
747            // A dialect with no delimited form (SOQL): the stored name is its only spelling.
748            return name;
749        }
750        return IdentifierCodec.encodeStored(profile, objectType, name);
751    }
752
753    /**
754     * Whether this environment holds any catalog metadata to check a table reference against:
755     * objects registered from a metadata file, from DDL in the script, or by the caller. An
756     * environment backed by a lazily loaded catalog overrides this, since its objects are not
757     * registered until they are asked for.
758     *
759     * <p>With nothing to check against, "this table is not in the catalog" would be reported
760     * for every table of every statement, which says nothing; such tables are
761     * {@code ETableCatalogStatus.NOT_CHECKED} instead.</p>
762     */
763    public boolean hasCatalogMetadata() {
764        return catalogProvider != null && catalogProvider.size() > 0;
765    }
766
767    /**
768     * Instance counterpart of {@link #toLexical(EDbVendor, ESQLDataObjectType, String,
769     * IdentifierInputForm)} that encodes under THIS environment's configuration.
770     *
771     * <p>Not a convenience alias: the static form states the vendor default, while a stored name
772     * entering this environment has to be encoded with the same profile the environment's indexes
773     * will key it under.
774     */
775    public String toLexical(ESQLDataObjectType objectType, String name, IdentifierInputForm form) {
776        if (name == null || name.isEmpty() || form != IdentifierInputForm.CATALOG_STORED
777                || objectType == null) {
778            return name;
779        }
780        // non-identifier-compare: this library's placeholders for a tier the dialect lacks.
781        if ((objectType == ESQLDataObjectType.dotCatalog && DEFAULT_DB_NAME.equals(name))
782                || (objectType == ESQLDataObjectType.dotSchema && DEFAULT_SCHEMA_NAME.equals(name))) {
783            return name;
784        }
785        if (!IdentifierCodec.hasQuotedForm(identifierProfile, objectType)) {
786            return name;
787        }
788        return IdentifierCodec.encodeStored(identifierProfile, objectType, name);
789    }
790
791    /**
792     * Read a metadata file's {@code nameForm} declaration.
793     *
794     * @param declared the raw value, may be null
795     * @param defaultForm the form this metadata FORMAT uses when a file does not declare one
796     * @throws InvalidNameFormException for a value that is neither form, so a typo cannot
797     *         silently select the default and change what every name in the file means
798     */
799    public static IdentifierInputForm parseNameForm(Object declared, IdentifierInputForm defaultForm) {
800        if (declared == null) return defaultForm;
801        String text = declared.toString().trim();
802        if (text.isEmpty()) return defaultForm;
803        try {
804            return IdentifierInputForm.valueOf(text.toUpperCase(java.util.Locale.ROOT));
805        } catch (IllegalArgumentException e) {
806            throw new InvalidNameFormException("Invalid nameForm '" + text
807                    + "'. Must be one of: SQL_LEXICAL, CATALOG_STORED");
808        }
809    }
810
811    /**
812     * A metadata file declared a {@code nameForm} that cannot be honored: a misspelt value, or
813     * {@code CATALOG_STORED} on a format whose loader only reads SQL spellings. It has its own
814     * type so a loader's catch-all cannot turn it into "no catalog loaded": without the catalog
815     * the resolver infers tables from usage and reports confident matches against the wrong one.
816     */
817    public static class InvalidNameFormException extends IllegalArgumentException {
818        private static final long serialVersionUID = 1L;
819
820        public InvalidNameFormException(String message) {
821            super(message);
822        }
823    }
824
825    /**
826     * For a metadata format whose loader reads every name as SQL text and has no conversion:
827     * accept an absent or {@code SQL_LEXICAL} declaration, refuse {@code CATALOG_STORED}.
828     *
829     * @param formatName the format, for the message
830     */
831    public static void requireSqlLexicalNameForm(Object declared, String formatName) {
832        if (parseNameForm(declared, IdentifierInputForm.SQL_LEXICAL) == IdentifierInputForm.CATALOG_STORED) {
833            throw new InvalidNameFormException("nameForm CATALOG_STORED is not supported by the "
834                    + formatName + " metadata format; write its names as SQL text or use the sqlflow"
835                    + " 'servers' or the JSON manifest format");
836        }
837    }
838
839    public String normalizeIdentifier(ESQLDataObjectType sqlDataObjectType, String identifier) {
840        return this.getIdentifierService().normalize(identifier,sqlDataObjectType);
841    }
842
843        /**
844         * Legacy single-identifier normalizer: strips the delimiter if quoted, otherwise
845         * folds case by a hard-coded vendor switch. It IGNORES {@code sqlDataObjectType}
846         * and bypasses the vendor {@link IdentifierProfile} (no per-object-type rules, no
847         * MySQL {@code lower_case_table_names}), so its output can disagree with the
848         * canonical engine.
849         *
850         * @deprecated For equality tests use
851         * {@link gudusoft.gsqlparser.util.SQLUtil#sameName(EDbVendor, ESQLDataObjectType,
852         * String, String)} or {@link gudusoft.gsqlparser.util.SQLUtil#compareIdentifier(
853         * EDbVendor, ESQLDataObjectType, String, String)} — never normalize-then-compare.
854         * For the stored/display form use
855         * {@link IdentifierService#normalizeStatic(EDbVendor, ESQLDataObjectType, String)};
856         * for map keys use {@link gudusoft.gsqlparser.util.SQLUtil#canonKey(EDbVendor,
857         * ESQLDataObjectType, String)} or {@link IdentifierService#keyForMap(String,
858         * ESQLDataObjectType)}.
859         */
860    @Deprecated
861    public static String normalizeIdentifier(EDbVendor dbVendor, ESQLDataObjectType sqlDataObjectType, String identifier){
862        String ret;
863
864        if (identifier == null) return identifier;
865        if (identifier.length() == 0) return identifier;
866
867        if (TSQLEnv.isDelimitedIdentifier(dbVendor,identifier)){
868            ret = TBaseType.getTextWithoutQuoted(identifier);
869        }else {
870            // Locale.ROOT: identifier folding must not depend on the JVM default locale.
871            switch (dbVendor){
872                case dbvdb2:
873                case dbvoracle:
874                    ret =  identifier.toUpperCase(Locale.ROOT);
875                    break;
876                case dbvdameng:
877                    ret =  identifier.toUpperCase(Locale.ROOT);
878                    break;
879                case dbvpostgresql:
880                case dbvduckdb:
881                case dbvgreenplum:
882                case dbvredshift:
883                    ret =  identifier.toLowerCase(Locale.ROOT);
884                    break;
885                default:
886                    ret =  identifier;
887                    break;
888            }
889        }
890        return ret;
891    }
892
893    /**
894     * 比较 一个数据库对象名是否等于或者属于另一个对象
895     * 等于 就是完全相等(根据不同数据库的比较规则)
896     * 属于 表示如下情况:
897     * 1. column1 -&gt; 属于 -&gt; table1.column1
898     * 2. table1 -&gt; 属于 -&gt; db1.schema1.table1
899     * 3. `schema1.table1` -&gt; 属于 -&gt; `db1`.`schema1`.`table1`
900     * 4. `schema1.table1` -&gt; 不属于 -&gt; `db1`.`schema2`.`table1`
901     *
902     * @param sub
903     * @param whole
904     * @return
905     */
906    public static boolean matchSubObjectNameToWhole(EDbVendor dbVendor, ESQLDataObjectType sqlDataObjectType,String sub, String whole){
907        List<String> subParts = SQLUtil.parseNames(sub);
908        // `data.RETAIL_PROD_EXCEPTIONS_SOURCE` 没有被分开,需要去掉 `` 后再次拆分
909        if ((subParts.size() == 1)&&(sub.indexOf(".") != -1)){
910            subParts = SQLUtil.parseNames(IdentifierService.normalizeStatic(dbVendor,sqlDataObjectType,sub));
911        }
912
913        List<String> wholeParts = SQLUtil.parseNames(whole);
914        if ((wholeParts.size() == 1)&&(whole.indexOf(".") != -1)){
915            wholeParts = SQLUtil.parseNames(IdentifierService.normalizeStatic(dbVendor,sqlDataObjectType,whole));
916        }
917
918
919        if(subParts.size() >wholeParts.size()) return false;
920        int k=0;
921        for(String s:subParts){
922            subParts.set(k,TBaseType.removePrefixOrSuffixQuoteChar(normalizeIdentifier(dbVendor,sqlDataObjectType,s)));
923            k++;
924        }
925        k=0;
926        for(String s:wholeParts){
927            wholeParts.set(k, TBaseType.removePrefixOrSuffixQuoteChar(normalizeIdentifier(dbVendor,sqlDataObjectType,s)));
928            k++;
929        }
930
931        boolean ret = false;
932        int i= subParts.size() -1;
933        int j = wholeParts.size() -1;
934        while (i >= 0){
935            if ( !subParts.get(i).toUpperCase(Locale.ROOT).equals(wholeParts.get(j).toUpperCase(Locale.ROOT))) break;
936            if (i == 0) ret = true;
937            i--;
938            j--;
939        }
940
941        return ret;
942    }
943
944    private boolean enableGetMetadataFromDDL = true;
945
946    public void setEnableGetMetadataFromDDL(boolean enableGetMetadataFromDDL) {
947        this.enableGetMetadataFromDDL = enableGetMetadataFromDDL;
948    }
949
950    /**
951     * If this option is enabled, SQLEnv will collect table/view/function/procedure metadata
952     * from the create table/create view/create function/create procedure statement during the parse of the SQL script.
953     * <br>A TSQLEnv object instance must be passed to {@link TGSqlParser} before parsing the SQL script. And this TSQLEnv
954     * instance can be passed to another {@link TGSqlParser} object with the collected database metadata.
955     * <br>Default value is true.
956     *
957     * @return
958     */
959    public boolean isEnableGetMetadataFromDDL() {
960        return enableGetMetadataFromDDL;
961    }
962
963    /**
964     * create a SQL environment.
965     *
966     * @param dbVendor the database vendor
967     */
968    public TSQLEnv(EDbVendor dbVendor){
969        this(dbVendor, IdentifierProfile.forVendor(dbVendor, IdentifierProfile.VendorFlags.defaults()));
970    }
971
972    /**
973     * Create a SQL environment whose identifier comparison, normalization and catalog indexes all
974     * run under ONE explicit configuration.
975     *
976     * <p>Use this when an analysis needs rules other than the vendor default — for example
977     * BigQuery table names compared case-insensitively while catalog, schema and column keep
978     * their documented rules:</p>
979     * <pre>
980     * IdentifierProfile profile = IdentifierProfile.builder(EDbVendor.dbvbigquery)
981     *         .withObjectRules(ESQLDataObjectType.dotTable,
982     *                 new IdentifierRules(CaseFold.NONE, CaseCompare.INSENSITIVE,
983     *                                     CaseFold.NONE, CaseCompare.INSENSITIVE))
984     *         .build();
985     * TSQLEnv env = new TSQLEnv(EDbVendor.dbvbigquery, profile) { public void initSQLEnv() {} };
986     * </pre>
987     *
988     * <p>The profile is fixed for the lifetime of this environment, deliberately: catalog indexes
989     * are built with it, so swapping it afterwards would leave objects registered under keys the
990     * new rules cannot reproduce. To analyze the same metadata under different rules, build a
991     * second environment and load the metadata again from its original input.</p>
992     *
993     * <p>The profile's vendor must be {@code dbVendor}; a mismatch is a configuration error and
994     * is reported rather than silently resolved in either direction.</p>
995     *
996     * @param dbVendor the database vendor
997     * @param identifierProfile the effective identifier configuration for this environment
998     * @throws IllegalArgumentException if the profile is null or belongs to another vendor
999     */
1000    public TSQLEnv(EDbVendor dbVendor, IdentifierProfile identifierProfile){
1001        if (identifierProfile == null) {
1002            throw new IllegalArgumentException("TSQLEnv: identifierProfile must not be null");
1003        }
1004        if (dbVendor != null && identifierProfile.getVendor() != dbVendor) {
1005            throw new IllegalArgumentException("TSQLEnv: identifier profile is for vendor "
1006                    + identifierProfile.getVendor() + " but this environment is " + dbVendor);
1007        }
1008        this.dbVendor = dbVendor;
1009
1010        // ===== Phase 0: Initialize IdentifierService first (required by CatalogStore) =====
1011        this.identifierProfile = identifierProfile;
1012        this.collatorProvider = IdentifierService.collatorProviderFor(dbVendor);  // SQL Server needs this
1013        this.identifierService = new IdentifierService(identifierProfile, collatorProvider);
1014
1015        // One service for the whole environment: the name service and the catalog store share the
1016        // instance, so nothing inside this environment can fall back to the vendor default.
1017        this.nameService = NameService.forIdentifierService(this.identifierService);
1018
1019        // Phase 3.5: Create CatalogStoreProvider (which internally creates CatalogStore)
1020        this.catalogProvider = new CatalogStoreProvider(this.identifierService);
1021    }
1022
1023    private EDbVendor dbVendor;
1024
1025    // Name matching service (Phase 2 integration)
1026    private final NameService nameService;
1027
1028    // ===== Phase 3.5: Final catalog implementation =====
1029    private IdentifierProfile identifierProfile;
1030    private IdentifierService identifierService;
1031    private CollatorProvider collatorProvider;  // SQL Server specific
1032    private ICatalogProvider catalogProvider;   // Uses CatalogStoreProvider
1033
1034    /**
1035     * the database vendor where this SQL environment is generated from.
1036     *
1037     * @return the database vendor
1038     */
1039    public EDbVendor getDBVendor() {
1040        return dbVendor;
1041    }
1042
1043    /**
1044     * Returns the catalog store (Phase 3.5 integration).
1045     *
1046     * <p>Note: This method delegates to the underlying CatalogStoreProvider.
1047     * Direct access to CatalogStore is provided for backward compatibility
1048     * with existing test code.</p>
1049     *
1050     * @return the catalog store
1051     */
1052    public CatalogStore getCatalogStore() {
1053        if (catalogProvider instanceof CatalogStoreProvider) {
1054            return ((CatalogStoreProvider) catalogProvider).getCatalogStore();
1055        }
1056        return null;
1057    }
1058
1059    // ===== Phase 0: Helper methods and getters =====
1060
1061    /**
1062     * Create CollatorProvider for SQL Server
1063     *
1064     * @param vendor database vendor
1065     * @return CollatorProvider instance, null for non-SQL Server databases
1066     */
1067    private CollatorProvider createCollatorProvider(EDbVendor vendor) {
1068        return IdentifierService.collatorProviderFor(vendor);
1069    }
1070
1071    /**
1072     * Get IdentifierService (Phase 0)
1073     *
1074     * @return IdentifierService instance
1075     */
1076    public IdentifierService getIdentifierService() {
1077        return identifierService;
1078    }
1079
1080    /**
1081     * Get IdentifierProfile (Phase 0)
1082     *
1083     * @return IdentifierProfile instance
1084     */
1085    public IdentifierProfile getIdentifierProfile() {
1086        return identifierProfile;
1087    }
1088
1089    /**
1090     * Get ICatalogProvider (Phase 3)
1091     *
1092     * @return ICatalogProvider instance
1093     */
1094    public ICatalogProvider getCatalogProvider() {
1095        return catalogProvider;
1096    }
1097
1098    /**
1099     * Catalog-input compatibility hook (Phase 1D).
1100     *
1101     * <p>Lets a {@code TSQLEnv} subclass in {@code gudusoft.gsqlparser.sqlenv.compat}
1102     * install an alternative {@link ICatalogProvider} (typically one backed by a
1103     * {@code CatalogRuntime}) without reflection or constructor re-entry. See spike
1104     * memo {@code docs/designs/catalog-input-interface-spikes/tsqlenv-hook.md}.</p>
1105     *
1106     * <p>{@code protected final} keeps the surface tight — only subclasses may invoke
1107     * it, and they cannot override it. The intended caller is
1108     * {@code CatalogRuntimeToSQLEnvBridge}. End users should continue to use the
1109     * legacy {@code TSQLEnv} mutation API; this hook is part of the catalog-input
1110     * compatibility bridge and is not a general extensibility point.</p>
1111     *
1112     * @param provider the replacement catalog provider; must not be {@code null}.
1113     */
1114    protected final void setCatalogProviderForCompatibility(ICatalogProvider provider) {
1115        if (provider == null) {
1116            throw new IllegalArgumentException(
1117                "TSQLEnv.setCatalogProviderForCompatibility: provider must not be null");
1118        }
1119        this.catalogProvider = provider;
1120    }
1121
1122    /**
1123     * This method must be override in the subclass to build a SQL environment with real metadata.
1124     * <br>this usually done by querying the INFORMATION_SCHEMA
1125     */
1126    public abstract void initSQLEnv();
1127
1128    /**
1129     * Create a deep copy of this TSQLEnv for use in concurrent threads.
1130     *
1131     * <p>The copy is independent and thread-safe: modifications to the copy
1132     * will not affect the original, and vice versa. This is essential when
1133     * multiple threads need to use the same metadata concurrently, since
1134     * {@code defaultCatalogName}/{@code defaultSchemaName} and internal
1135     * catalog/schema structures are mutated during analysis.</p>
1136     *
1137     * @return a deep copy of this TSQLEnv
1138     */
1139    public TSQLEnv copy() {
1140        // The copy carries THIS environment's identifier configuration, not the vendor default:
1141        // a parallel worker handed a copy must key its catalog exactly as the analysis that
1142        // produced the metadata did, or the same reference resolves differently per thread.
1143        TSQLEnv copyEnv = new TSQLEnv(this.dbVendor, this.identifierProfile) {
1144            @Override
1145            public void initSQLEnv() {
1146            }
1147        };
1148
1149        copyEnv.setDefaultCatalogName(this.defaultCatalogName);
1150        copyEnv.setDefaultSchemaName(this.defaultSchemaName);
1151        copyEnv.setDefaultServerName(this.defaultServerName);
1152
1153        if (this.catalogList != null) {
1154            for (TSQLCatalog catalog : this.catalogList) {
1155                copyCatalog(catalog, copyEnv);
1156            }
1157        }
1158
1159        return copyEnv;
1160    }
1161
1162    /**
1163     * The spelling an object of the copy is created from: the one its original was created from.
1164     *
1165     * <p>The creation entry points read every name as SQL text, while {@link TSQLObject#getName()}
1166     * is the name as stored. Handing the stored name back folds it a second time: PostgreSQL
1167     * {@code "Amount"} is stored as {@code Amount}, read back as SQL it becomes {@code amount},
1168     * and the copy merges it into the real {@code amount}. The kept spelling is the text the
1169     * original was created from, so the copy keys and names every object as the original does.
1170     */
1171    private static String creationSpelling(TSQLObject source) {
1172        return source.getNameKeepCase() != null ? source.getNameKeepCase() : source.getName();
1173    }
1174
1175    private static void copyCatalog(TSQLCatalog source, TSQLEnv targetEnv) {
1176        TSQLCatalog target = targetEnv.createSQLCatalog(creationSpelling(source));
1177        List<TSQLSchema> schemas = source.getSchemaList();
1178        if (schemas != null) {
1179            for (TSQLSchema schema : schemas) {
1180                copySchema(schema, target);
1181            }
1182        }
1183    }
1184
1185    private static void copySchema(TSQLSchema source, TSQLCatalog targetCatalog) {
1186        TSQLSchema target = targetCatalog.getSchema(creationSpelling(source), true);
1187        List<TSQLSchemaObject> objects = source.getSchemaObjectList();
1188        if (objects != null) {
1189            for (TSQLSchemaObject obj : objects) {
1190                copySchemaObject(obj, target);
1191            }
1192        }
1193    }
1194
1195    private static void copySchemaObject(TSQLSchemaObject source, TSQLSchema targetSchema) {
1196        if (source instanceof TSQLTable) {
1197            TSQLTable sourceTable = (TSQLTable) source;
1198            TSQLTable targetTable = targetSchema.createTable(creationSpelling(sourceTable), sourceTable.getPriority());
1199            targetTable.setView(sourceTable.isView());
1200            if (sourceTable.getDefinition() != null) {
1201                targetTable.setDefinition(sourceTable.getDefinition());
1202            }
1203            List<TSQLColumn> columns = sourceTable.getColumnList();
1204            if (columns != null) {
1205                for (TSQLColumn col : columns) {
1206                    if (col.isUnresolvedStar()) {
1207                        // a star the table's width could not be resolved from, not a column named *
1208                        targetTable.addUnresolvedStar();
1209                    } else if (col.getColumnDataType() != null) {
1210                        targetTable.addColumn(creationSpelling(col), col.getColumnDataType());
1211                    } else {
1212                        targetTable.addColumn(creationSpelling(col));
1213                    }
1214                }
1215            }
1216        } else if (source instanceof TSQLRoutine) {
1217            TSQLRoutine sourceRoutine = (TSQLRoutine) source;
1218            TSQLSchemaObject targetRoutine = targetSchema.createSchemaObject(
1219                    creationSpelling(sourceRoutine), sourceRoutine.getDataObjectType());
1220            if (targetRoutine instanceof TSQLRoutine) {
1221                copyRoutine(sourceRoutine, (TSQLRoutine) targetRoutine);
1222            }
1223        } else if (source instanceof TSQLSynonyms) {
1224            TSQLSynonyms sourceSynonyms = (TSQLSynonyms) source;
1225            TSQLSynonyms targetSynonyms = targetSchema.createSynonyms(
1226                    creationSpelling(sourceSynonyms),
1227                    sourceSynonyms.getSourceDatabase(),
1228                    sourceSynonyms.getSourceSchema(),
1229                    sourceSynonyms.getSourceName()
1230            );
1231            if (targetSynonyms != null) {
1232                targetSynonyms.setPriority(sourceSynonyms.getPriority());
1233            }
1234        }
1235    }
1236
1237    /**
1238     * A routine's own state: its priority, definition and parameters, a function's return type and the columns of
1239     * the table it returns, and the procedures, functions and triggers of an Oracle package.
1240     */
1241    private static void copyRoutine(TSQLRoutine source, TSQLRoutine target) {
1242        target.setPriority(source.getPriority());
1243        if (source.getDefinition() != null) {
1244            target.setDefinition(source.getDefinition());
1245        }
1246        List<TSQLParameter> params = source.getParameterList();
1247        if (params != null) {
1248            for (TSQLParameter param : params) {
1249                target.addParameter(param.getParameterIndex(),
1250                        param.getParameterType(),
1251                        creationSpelling(param));
1252            }
1253        }
1254        // the values preset for its parameters (immutable values): a worker of a parallel analysis sees them too
1255        for (Map.Entry<String, TSQLRoutine.PresetValue> preset : source.getParameterPresets().entrySet()) {
1256            target.setParameterPreset(preset.getKey(), preset.getValue());
1257        }
1258        if (source instanceof TSQLFunction && target instanceof TSQLFunction) {
1259            TSQLFunction sourceFunction = (TSQLFunction) source;
1260            TSQLFunction targetFunction = (TSQLFunction) target;
1261            targetFunction.setReturnType(sourceFunction.getReturnType());
1262            for (TFunctoinColumn column : sourceFunction.getColumnsInReturnTable()) {
1263                targetFunction.addReturnColumn(creationSpelling(column));
1264            }
1265        }
1266        if (source instanceof TSQLOraclePackage && target instanceof TSQLOraclePackage) {
1267            TSQLOraclePackage targetPackage = (TSQLOraclePackage) target;
1268            for (TSQLSchemaObject member : ((TSQLOraclePackage) source).getSchemaObjectList()) {
1269                TSQLRoutine copy = null;
1270                if (member.getDataObjectType() == ESQLDataObjectType.dotFunction) {
1271                    copy = targetPackage.createFunction(creationSpelling(member));
1272                } else if (member.getDataObjectType() == ESQLDataObjectType.dotTrigger) {
1273                    copy = targetPackage.createTrigger(creationSpelling(member));
1274                } else if (member.getDataObjectType() == ESQLDataObjectType.dotProcedure) {
1275                    copy = targetPackage.createProcedure(creationSpelling(member));
1276                }
1277                if (copy != null && member instanceof TSQLRoutine) {
1278                    copyRoutine((TSQLRoutine) member, copy);
1279                }
1280            }
1281        }
1282    }
1283
1284    /**
1285     * a list of catalog/database in this SQL environment.
1286     *
1287     * @return a list of catalog/database
1288     */
1289    public List<TSQLCatalog> getCatalogList() {
1290        return catalogList;
1291    }
1292
1293    private String serverName = null; // SQL Server
1294
1295    private List<TSQLCatalog> catalogList = new CopyOnWriteArrayList<>();
1296
1297    /**
1298     * add a catalog to the SQL environment, called internally.
1299     *
1300     * @param sqlCatalog catalog
1301     * @return return false if a catalog with the same name already exists.
1302     */
1303    protected boolean doAddCatalog(TSQLCatalog sqlCatalog){
1304        // same monitor as getSQLCatalog so the check-then-add is atomic under
1305        // concurrent registration; readers iterate the COW list lock-free
1306        synchronized (catalogList) {
1307            boolean isFound = false;
1308            for(TSQLCatalog c : catalogList){
1309                if ( sqlCatalog.getName().compareTo(c.getName()) == 0){
1310                    isFound = true;
1311                    break;
1312                }
1313            }
1314            if (!isFound){
1315                catalogList.add(sqlCatalog);
1316            }
1317            return !isFound;
1318        }
1319    }
1320
1321    /**
1322     * put a schema object into the hashmap which can be used to find a schema object in a more efficient way.
1323     *
1324     * @param schemaObjectName  name of schema object
1325     * @param schemaObject instance of a schema object
1326     * @return always return true.
1327     */
1328    protected boolean putSchemaObject(String schemaObjectName, TSQLSchemaObject schemaObject){
1329        // The catalog provider is the only index. It keys an object by the name service's
1330        // canonical key of each of its own names (CatalogStore.createKey), so schemaObjectName,
1331        // a bare join of normalized names that cannot be split back, is not used for keying.
1332        //
1333        // Three more indexes used to be maintained here from that string (schemaObjectList,
1334        // objectIndex, tablesByName). Re-parsing it folded names a second time, so Oracle
1335        // "Orders" and ORDERS shared one key, MSSQL [my.db].dbo.t2 was filed under catalog my /
1336        // schema db, and "a.b" was findable as b. They only ever answered after the provider had
1337        // correctly said "not there".
1338        try {
1339            catalogProvider.addObject(schemaObject);
1340        } catch (Throwable e) {
1341            // Log error but maintain backward compatibility
1342            logger.error("[CatalogProvider] Error adding object: " + schemaObject + ", error=" + e.getMessage(), e);
1343        }
1344        return true;
1345    }
1346
1347    /**
1348     * add a table
1349     *
1350     * @param qualifiedTableName, must be in syntax like: catalog.schema.table
1351     * @param sqlTable, table instance
1352     */
1353//    public void addSQLTable(String qualifiedTableName, TSQLTable sqlTable){
1354//        schemaObjectList.put(qualifiedTableName,sqlTable);
1355//    }
1356//
1357//    public void addRoutine(String qualifiedRoutineName, TSQLRoutine sqlRoutine){
1358//        schemaObjectList.put(qualifiedRoutineName,sqlRoutine);
1359//    }
1360
1361    public static final String DEFAULT_SERVER_NAME = "DEFAULT_SERVER";
1362    public static final String DEFAULT_DB_NAME = "DEFAULT";
1363    public static final String DEFAULT_SCHEMA_NAME = "DEFAULT";
1364
1365    private String defaultCatalogName = null;
1366    private String defaultSchemaName = null;
1367    private String defaultServerName = null;
1368
1369    protected TSQLSchemaObject doSearchSchemaObject(String catalog, String schema, String table, ESQLDataObjectType objectType){
1370        // catalog / schema / table arrive ALREADY NORMALIZED, i.e. they are stored-form names.
1371        // They go to the provider as they are. This method used to join them with dots and hand
1372        // the string to an overload that parses SQL text, which folded them a second time:
1373        // Oracle "Orders" -> Orders -> (re-read as the unquoted word) ORDERS, so a quoted table
1374        // could not be found by name, and a dot inside a name split it into extra segments.
1375        TSQLSchemaObject result = null;
1376
1377        String normalizedCurrentCatalogName = this.normalizeIdentifier(ESQLDataObjectType.dotCatalog, defaultCatalogName);
1378        String normalizedCurrentSchemaName = this.normalizeIdentifier(ESQLDataObjectType.dotSchema, defaultSchemaName);
1379
1380        if ((catalog.length()>0)&&(schema.length()>0)){ // catalog.schema.table
1381            result = findNormalized(catalog, schema, table, objectType);
1382        }else if (schema.length()>0){ //.schema.table
1383            if (defaultCatalogName != null){
1384                result = findNormalized(normalizedCurrentCatalogName, schema, table, objectType);
1385            }else{
1386                for(TSQLCatalog c : catalogList){
1387                    result = findNormalized(c.name, schema, table, objectType);
1388                    if (result != null) break;
1389                }
1390            }
1391        }else if (catalog.length()>0){ // catalog..table
1392            if (defaultSchemaName != null){
1393                result = findNormalized(catalog, normalizedCurrentSchemaName, table, objectType);
1394            }else{
1395                for(TSQLCatalog c : catalogList){
1396                    if ( c.compareTo(catalog) != 0) continue;
1397                    for(TSQLSchema s: c.getSchemaList()){
1398                        result = findNormalized(c.name, s.name, table, objectType);
1399                        if (result != null) break;
1400                    }
1401                    if (result != null) break;
1402                }
1403            }
1404        }else{ // ..table, search under the current default database and schema.
1405            // The current default database and schema can be set by the user.
1406            // if current default database and schema is not set, then search in all databases and schemas
1407            for(TSQLCatalog c : catalogList){
1408                if ((defaultCatalogName != null) && ( c.compareTo(defaultCatalogName) != 0)) continue;
1409                for(TSQLSchema s: c.getSchemaList()){
1410                    if ((defaultSchemaName != null)&&(s.compareTo(defaultSchemaName) != 0)) continue;
1411                    result = findNormalized(c.name, s.name, table, objectType);
1412                    if (result != null) break;
1413                }
1414                if (result != null) break;
1415            }
1416        }
1417
1418        return result;
1419    }
1420
1421    /** One provider lookup with already normalized segments; a provider failure is a miss. */
1422    private TSQLSchemaObject findNormalized(String catalog, String schema, String object, ESQLDataObjectType objectType) {
1423        try {
1424            return catalogProvider.findObject(catalog, schema, object, objectType);
1425        } catch (RuntimeException e) {
1426            logger.error("[CatalogProvider] Error finding object: " + object + ", error=" + e.getMessage(), e);
1427            return null;
1428        }
1429    }
1430
1431    protected TSQLSchemaObject searchSchemaObject(TObjectName qualifiedName, ESQLDataObjectType objectType){
1432
1433        String catalog = this.normalizeIdentifier(ESQLDataObjectType.dotCatalog, qualifiedName.getDatabaseString());
1434        String schema = this.normalizeIdentifier(ESQLDataObjectType.dotSchema, qualifiedName.getSchemaString());
1435        String table = this.normalizeIdentifier(ESQLDataObjectType.dotTable, qualifiedName.getTableString());
1436
1437        schema = (schema == null)?getDefaultSchemaName():schema;
1438        schema = (schema == null)?"":schema;
1439
1440        catalog = (catalog == null)?getDefaultCatalogName():catalog;
1441        catalog = (catalog == null)?"":catalog;
1442
1443        return doSearchSchemaObject(catalog,schema,table,objectType);
1444    }
1445
1446    /**
1447     * Phase 1: 使用 IdentifierService 的新方法处理多段名
1448     */
1449    public TSQLSchemaObject searchSchemaObject(String qualifiedName, ESQLDataObjectType objectType){
1450        // Phase 1: 使用 IdentifierService 的新方法
1451        IdentifierService svc = getIdentifierService();
1452
1453        // 1. 解析多段名
1454        List<String> parts = svc.parseQualifiedName(qualifiedName);
1455
1456        // 2. 展开厂商特定语法(MSSQL "..")
1457        // Pass the default schema name to avoid relying on global state
1458        parts = svc.expandVendorSpecific(parts, dbVendor, getDefaultSchemaName());
1459
1460                if (parts.size() < 3) {
1461                        return null;
1462                }
1463
1464                boolean supportCatalog = TSQLEnv.supportCatalog(dbVendor);
1465                boolean supportSchema = TSQLEnv.supportSchema(dbVendor);
1466
1467        // 3. 使用 normalizeSegment 规范化各段(Phase 1: 单段处理)
1468        String catalog = svc.normalizeSegment(parts.get(0), ESQLDataObjectType.dotCatalog);
1469        String schema = svc.normalizeSegment(parts.get(1), ESQLDataObjectType.dotSchema);
1470        String table = svc.normalizeSegment(SQLUtil.mergeSegments(parts, 2), ESQLDataObjectType.dotTable);
1471
1472        TSQLSchemaObject object = doSearchSchemaObject(catalog,schema,table,objectType);
1473
1474        //如果不是同时支持database和schema,且没有找到object,交换database和schema顺序查找
1475                if (!(supportCatalog && supportSchema) && object == null) {
1476                        catalog = svc.normalizeSegment(parts.get(1), ESQLDataObjectType.dotCatalog);
1477                        schema = svc.normalizeSegment(parts.get(0), ESQLDataObjectType.dotSchema);
1478                        object = doSearchSchemaObject(catalog, schema, table, objectType);
1479                }
1480
1481                return object;
1482
1483//        String normalizedCurrentCatalogName = TSQLObject.normalizeIdentifier(this,ESQLDataObjectType.dotCatalog, defaultCatalogName);
1484//        String normalizedCurrentSchemaName = TSQLObject.normalizeIdentifier(this,ESQLDataObjectType.dotSchema, defaultSchemaName);
1485//
1486//        if ((catalog.length()>0)&&(schema.length()>0)){ // catalog.schema.table
1487//            result = doSearchSchemaObject(catalog+"."+schema+"."+table,objectType);
1488//        }else if (schema.length()>0){ //.schema.table
1489//            if (defaultCatalogName != null){
1490//                result = doSearchSchemaObject(normalizedCurrentCatalogName+"."+schema+"."+table,objectType);
1491//            }else{
1492//                for(TSQLCatalog c : catalogList){
1493//                    result = doSearchSchemaObject(c.name+"."+schema+"."+table,objectType);
1494//                    if (result != null) break;
1495//                }
1496//            }
1497//        }else if (catalog.length()>0){ // catalog..table
1498//            if (defaultSchemaName != null){
1499//                result = doSearchSchemaObject(catalog+"."+normalizedCurrentSchemaName+"."+table,objectType);
1500//            }else{
1501//                for(TSQLCatalog c : catalogList){
1502//                    if ( c.compareTo(catalog) != 0) continue;
1503//                    for(TSQLSchema s: c.getSchemaList()){
1504//                        result = doSearchSchemaObject(s.getQualifiedName()+"."+table,objectType);
1505//                        if (result != null) break;
1506//                    }
1507//                    if (result != null) break;
1508//                }
1509//            }
1510//        }else{ // ..table
1511//            for(TSQLCatalog c : catalogList){
1512//                if ((defaultCatalogName != null) && ( c.compareTo(defaultCatalogName) != 0)) continue;
1513//                for(TSQLSchema s: c.getSchemaList()){
1514//                    if ((defaultSchemaName != null)&&(s.compareTo(defaultSchemaName) != 0)) continue;
1515//                    result = doSearchSchemaObject(s.getQualifiedName()+"."+table,objectType);
1516//                    if (result != null) break;
1517//                }
1518//                if (result != null) break;
1519//            }
1520//        }
1521//
1522//        return result;
1523
1524    }
1525    /**
1526     * find a table in the SQL environment by using a qualified table name: catalogName.schemaName.tableName
1527     *
1528     * @param qualifiedTablename, can be catalog.schema.table,
1529     *                            or .schema.table, use currentCatalogName or iterate all catalogs
1530     *                            or catalog..table, use current schema name or iterate all schema under catalog
1531     *                            or ..table, use currentCatalogName or iterate all catalogs, use current schema
1532     *                                          or iterate all schema
1533     * @return a table
1534     */
1535    public TSQLTable searchTable(String qualifiedTablename){
1536        TSQLSchemaObject result = searchSchemaObject(qualifiedTablename,dotTable);
1537
1538        if (result instanceof TSQLTable){
1539            return (TSQLTable)result;
1540        }
1541
1542        // The name may be a synonym; dereference it to its base table/view so a
1543        // synonym reference follows through to its base for column lineage.
1544        TSQLSchemaObject synonym = searchSchemaObject(qualifiedTablename, ESQLDataObjectType.dotSynonyms);
1545        if (synonym instanceof TSQLSynonyms){
1546            return ((TSQLSynonyms) synonym).resolveBaseTable();
1547        }
1548        TSQLCatalog unnamed = unnamedDatabase();
1549        if (unnamed == null) return null;
1550        List<String> parts = getIdentifierService().expandVendorSpecific(
1551                getIdentifierService().parseQualifiedName(qualifiedTablename), dbVendor, getDefaultSchemaName());
1552        return parts.size() == 3 ? searchInUnnamedDatabase(unnamed, parts.get(1), parts.get(2)) : null;
1553    }
1554
1555    public TSQLTable searchTable(TObjectName tableName){
1556        String catalogName = tableName.getDatabaseToken() == null ? getDefaultCatalogName() : tableName.getDatabaseString();
1557        String schemaName = tableName.getSchemaToken() == null ? getDefaultSchemaName() : tableName.getSchemaString();
1558        if (schemaName == null && (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql)) schemaName = "dbo";
1559        // Keep SQL spellings intact all the way to each catalog's own index. Joining
1560        // normalized segments into SQL text decodes/folds delimited names twice.
1561        for (TSQLCatalog catalog : catalogList) {
1562            if (catalogName != null && catalog.compareTo(catalogName) != 0) continue;
1563            for (TSQLSchema schema : catalog.getSchemaList()) {
1564                if (schemaName != null && schema.compareTo(schemaName) != 0) continue;
1565                TSQLTable table = schema.findTable(tableName.getTableString());
1566                if (table != null) return table;
1567                TSQLSynonyms synonym = schema.findSynonyms(tableName.getTableString());
1568                if (synonym != null) return synonym.resolveBaseTable();
1569                table = searchProviderTable(catalog.getName(), schema.getName(),
1570                        getIdentifierService().normalizeSegment(tableName.getTableString(), dotTable));
1571                if (table != null) return table;
1572            }
1573        }
1574        // An explicit/default schema need not have been materialized in a lazy env yet.
1575        if (catalogName != null && schemaName != null) {
1576            TSQLTable found = searchProviderTable(getIdentifierService().normalizeSegment(catalogName, dotCatalog),
1577                    getIdentifierService().normalizeSegment(schemaName, dotSchema),
1578                    getIdentifierService().normalizeSegment(tableName.getTableString(), dotTable));
1579            // A linked server or a database link is never the session's current database.
1580            if (found != null || tableName.getServerToken() != null || tableName.getDblink() != null) return found;
1581            TSQLCatalog unnamed = unnamedDatabase();
1582            return unnamed == null ? null : searchInUnnamedDatabase(unnamed, schemaName, tableName.getTableString());
1583        }
1584        return null;
1585    }
1586
1587    /**
1588     * The placeholder catalog holding what was registered without a database, when a reference that names a
1589     * database may meet it ({@link #searchInUnnamedDatabase}): null when a default catalog is set, when the
1590     * dialect has no schema level, or when nothing was registered without a database.
1591     */
1592    private TSQLCatalog unnamedDatabase() {
1593        if (defaultCatalogName != null || !supportSchema(dbVendor)) return null;
1594        return searchCatalog(DEFAULT_DB_NAME);
1595    }
1596
1597    /**
1598     * The relation {@code schemaName.objectName} registered without a database, for a reference that names a
1599     * database which does not hold it (Mantis 4751).
1600     *
1601     * <p>DDL or metadata that does not name its database ({@code CREATE TABLE DW_RE0.BusinessOwners}) is
1602     * registered in the placeholder catalog {@link #DEFAULT_DB_NAME} while no default catalog is set: its
1603     * database is the session's current one, which nothing here records. A reference that spells a database
1604     * ({@code dw_dina.DW_RE0.BusinessOwners}) can only meet that relation there, so after every exact lookup
1605     * missed, the same schema and name are looked up in the placeholder. Only objects this environment holds
1606     * there are read; a lazily loaded catalog is not asked. With a default catalog set, the current database is
1607     * known and unqualified names were registered in it, so a reference to another database must not reach
1608     * them.</p>
1609     *
1610     * <p>A best match, not a proof: two references naming different databases both meet the one relation, and
1611     * a database really named {@code DEFAULT} cannot be told from the placeholder.</p>
1612     *
1613     * @param unnamed the placeholder catalog, from {@link #unnamedDatabase()}
1614     * @param schemaName the schema as the reference spells it
1615     * @param objectName the table, view or synonym as the reference spells it
1616     */
1617    private TSQLTable searchInUnnamedDatabase(TSQLCatalog unnamed, String schemaName, String objectName) {
1618        TSQLSchema schema = unnamed.getSchema(schemaName, false);
1619        if (schema == null) return null;
1620        TSQLTable table = schema.findTable(objectName);
1621        if (table != null) return table;
1622        TSQLSynonyms synonym = schema.findSynonyms(objectName);
1623        return synonym == null ? null : synonym.resolveBaseTable();
1624    }
1625
1626    /** Provider input is already segmented and stored; never reconstruct SQL from it. */
1627    private TSQLTable searchProviderTable(String catalog, String schema, String object) {
1628        TSQLSchemaObject table = catalogProvider.findObject(catalog, schema, object, dotTable);
1629        if (table instanceof TSQLTable) return (TSQLTable) table;
1630        TSQLSchemaObject synonym = catalogProvider.findObject(catalog, schema, object, dotSynonyms);
1631        return synonym instanceof TSQLSynonyms ? ((TSQLSynonyms) synonym).resolveBaseTable() : null;
1632    }
1633    /**
1634     * called by {@link #searchTable(String)} method internally.
1635     *
1636     * @param qualifiedName table name
1637     * @return return a table instance if found, otherwise, return null
1638     */
1639//    TSQLTable doSearchTable(String qualifiedTablename){
1640//        TSQLTable result = null;
1641//        TSQLSchemaObject schemaObject = schemaObjectList.get(qualifiedTablename);
1642//
1643//        if (schemaObject instanceof TSQLTable){
1644//            result = (TSQLTable)schemaObject;
1645//        }
1646//        return result;
1647//    }
1648
1649
1650    /**
1651     * the current active catalog/database in the SQL environment.
1652     * If search a table in syntax like this:  .schemaName.tableName and this current catalog name is not null,
1653     * then, search the table in this syntax: currentCatalogName.schemaName.tableName.
1654     * If the current catalog name is null, search all catalogs in the catalog list.
1655     *
1656     * @return the name of the current active catalog/database
1657     */
1658    public String getDefaultCatalogName() {
1659        return defaultCatalogName;
1660    }
1661
1662    /**
1663     * the current schema name in the SQL environment.
1664     * <br>
1665     * If search a table in syntax like this:  catalogname..tableName and this current schema name is not null,
1666     * then, search the table in this syntax: catalogName.currentSchemaName.tableName.
1667     * If the current schema name is null, search all schemas under catalogName
1668     *
1669     * @return the default schema name
1670     */
1671    public String getDefaultSchemaName() {
1672//        if ((dbVendor == EDbVendor.dbvmssql)&&(defaultSchemaName == null)){
1673//            return "dbo";
1674//        }
1675        return defaultSchemaName;
1676    }
1677
1678    public void setDefaultCatalogName(String defaultCatalogName) {
1679        this.defaultCatalogName = defaultCatalogName;
1680    }
1681
1682    public void setDefaultSchemaName(String defaultSchemaName) {
1683        this.defaultSchemaName = defaultSchemaName;
1684    }
1685
1686    public String getDefaultServerName() {
1687        return defaultServerName;
1688    }
1689
1690    public void setDefaultServerName(String defaultServerName) {
1691        this.defaultServerName = defaultServerName;
1692    }
1693
1694
1695
1696    /**
1697     * create a new catalog/database in the SQL environment.
1698     *
1699     * @param catalogName catalog name
1700     * @return instance of the created catalog
1701     */
1702    public TSQLCatalog createSQLCatalog(String catalogName){
1703        return getSQLCatalog(catalogName,true);
1704    }
1705
1706    /**
1707     * get a catalog from the SQL environment if already exists, otherwise, create a new catalog.
1708     *
1709     * @param catalogName catalog name
1710     * @param createIfNotExist if this value is true, then create a new catalog if it's not exists
1711     * @return a catalog instance
1712     */
1713    public TSQLCatalog getSQLCatalog(String catalogName, boolean createIfNotExist){
1714        TSQLCatalog result = searchCatalog(catalogName);
1715        if ((result == null)&&(createIfNotExist)){
1716            // synchronized so concurrent get-or-create of the same catalog can't
1717            // produce duplicate twins (searchCatalog stays lock-free on the COW list)
1718            synchronized (catalogList) {
1719                result = searchCatalog(catalogName);
1720                if (result == null){
1721                    result = new TSQLCatalog(this,catalogName);
1722                }
1723            }
1724        }
1725        return result;
1726    }
1727
1728    /**
1729     * search catalog in the catalog list, return null if not found.
1730     *
1731     * @param catalogName catalog name
1732     * @return null if not found.
1733     */
1734    /**
1735     * Snowflake's own name for the schema a session falls into after a database
1736     * switch. Not a default we may assume: only used when this environment
1737     * proves the schema exists.
1738     *
1739     * @see #findSchemaAfterDatabaseSwitch(String)
1740     */
1741    public static final String SNOWFLAKE_SCHEMA_AFTER_DATABASE_SWITCH = "PUBLIC";
1742
1743    /**
1744     * The schema a Snowflake session lands in after {@code USE <database>}, when
1745     * this environment can PROVE it exists.
1746     *
1747     * <p>Snowflake sets the current schema to {@code PUBLIC} on a database
1748     * switch, or leaves it unset when the new database has no {@code PUBLIC}.
1749     * Whichever it is, the schema of the PREVIOUS database is gone - keeping it
1750     * would qualify later names as {@code <newdb>.<oldschema>}, a pair that need
1751     * not exist. But when a catalog is attached and it does contain
1752     * {@code PUBLIC}, the new schema is not a guess: it is a fact, and dropping
1753     * it would throw away a correct, exact resolution.
1754     *
1755     * @param databaseName the database being switched to
1756     * @return the schema's name as the catalog spells it, or null when nothing
1757     *         here proves one exists - callers then leave the schema unset
1758     *         rather than assuming {@code PUBLIC}. GitHub #715 / MantisBT 4677.
1759     */
1760    public String findSchemaAfterDatabaseSwitch(String databaseName){
1761        if (SQLUtil.isEmpty(databaseName)) return null;
1762        TSQLCatalog catalog = searchCatalog(databaseName);
1763        if (catalog == null) return null;
1764        TSQLSchema schema = catalog.getSchema(SNOWFLAKE_SCHEMA_AFTER_DATABASE_SWITCH, false);
1765        return (schema == null) ? null : schema.getName();
1766    }
1767
1768    public TSQLCatalog searchCatalog(String catalogName){
1769        TSQLCatalog result = null;
1770        for(TSQLCatalog c : catalogList){
1771            if (c.compareTo(catalogName)==0){
1772                result = c;
1773                break;
1774            }
1775        }
1776        return result;
1777    }
1778
1779    /**
1780     * create a new schema and add to the catalog
1781     *
1782     * @param qualifiedSchemaName must be a qualified name like: catalog.schema. Otherwise, a null exception will be raised.
1783     * @return a new schema instance
1784     */
1785    public TSQLSchema createSQLSchema(String qualifiedSchemaName){
1786        return  getSQLSchema(qualifiedSchemaName,true);
1787    }
1788
1789    /**
1790     * get a schema from the specified catalog, if not exists, create a new schema in the catalog.
1791     *
1792     * @param qualifiedSchemaName must be a qualified name like: catalog.schema. Otherwise, a null exception will be raised.
1793     * @param createIfNotExist  if this value is true, then create a new schema if it's not exists
1794     * @return a schema instance
1795     */
1796    public TSQLSchema getSQLSchema(String qualifiedSchemaName, boolean createIfNotExist){
1797        TSQLSchema result = null;
1798        String[] parts = SQLUtil.parseNames(qualifiedSchemaName).toArray(new String[0]);
1799        String catalogName = parts[0];
1800        String schemaName = parts[1];
1801        TSQLCatalog catalog = getSQLCatalog(catalogName,createIfNotExist);
1802        return  catalog.getSchema(schemaName,createIfNotExist);
1803    }
1804
1805
1806    /**
1807     *
1808     * @param qualifiedTablename 需要完整的tablename,例如 catalog.schema.table, 如果仅传入 table name,
1809     *                           需要利用 default catalog, default schema 拼接完整的名称: catalog.schema.table
1810     * @param columnNameOnly
1811     * @return
1812     */
1813    public  ArrayList<String> getColumnsInTable(String qualifiedTablename,boolean columnNameOnly){
1814
1815        //TSQLTable tsqlTable = searchTable(getAFullQualifiedSchemaObjectName(qualifiedTablename));
1816        TSQLTable tsqlTable = searchTable(qualifiedTablename);
1817        if (tsqlTable == null) return null;
1818        //return tsqlTable.searchColumn(columnName);
1819        return  tsqlTable.getColumns(columnNameOnly);
1820    }
1821
1822    public boolean columnInTable(String qualifiedTablename, String columnName){
1823        TSQLTable tsqlTable = searchTable(qualifiedTablename);
1824        if (tsqlTable == null) return false;
1825        return tsqlTable.searchColumn(columnName);
1826    }
1827
1828    public TSQLColumn getColumnInTable(String qualifiedTablename, String columnName){
1829        StringBuilder builder = new StringBuilder();
1830        String db = (getDatabaseName(qualifiedTablename) == null)?getDefaultCatalogName():getDatabaseName(qualifiedTablename);
1831        if((db == null) || (db.length() == 0)) {
1832                builder.append(DEFAULT_DB_NAME).append(".");
1833        }
1834        
1835        String schema = (getSchemaName(qualifiedTablename) == null)?getDefaultSchemaName():getSchemaName(qualifiedTablename);
1836        if ((schema == null)||(schema.length() == 0)) {
1837                builder.append(DEFAULT_SCHEMA_NAME).append(".");
1838        }
1839        
1840        builder.append(qualifiedTablename);
1841        
1842        TSQLTable tsqlTable = searchTable(builder.toString());
1843        if (tsqlTable == null) return null;
1844        return tsqlTable.getColumn(columnName);
1845    }
1846
1847//    String[] getCatalogSchemaNameWithDefault(String qualifiedObjectName){
1848//        String[] catalogSchemaName = new String[2];
1849//
1850//        if (qualifiedObjectName.startsWith("`") && qualifiedObjectName.endsWith("`")){
1851//            qualifiedObjectName = qualifiedObjectName.substring(1,qualifiedObjectName.length()-1);
1852//        }
1853//
1854//        String db = (getDatabaseName(qualifiedObjectName) == null)?getDefaultCatalogName():getDatabaseName(qualifiedObjectName);
1855//        if((db == null) || (db.length() == 0)) db = DEFAULT_DB_NAME;
1856//        String schema = (getSchemaName(qualifiedObjectName) == null)?getDefaultSchemaName():getSchemaName(qualifiedObjectName);
1857//        if ((schema == null)||(schema.length() == 0)) schema = DEFAULT_SCHEMA_NAME;
1858//
1859//        catalogSchemaName[0] = db;
1860//        catalogSchemaName[1] = schema;
1861//
1862//        return  catalogSchemaName;
1863//    }
1864    public TSQLSchemaObject doAddSchemaObject(String qualifiedObjectName,ESQLDataObjectType objectType){
1865
1866        if (qualifiedObjectName.startsWith("`") && qualifiedObjectName.endsWith("`")){
1867            qualifiedObjectName = SQLUtil.trimColumnStringQuote(qualifiedObjectName);
1868        }
1869
1870        String db = (getDatabaseName(qualifiedObjectName) == null)?getDefaultCatalogName():getDatabaseName(qualifiedObjectName);
1871        if((db == null) || (db.length() == 0)) db = DEFAULT_DB_NAME;
1872        String schema = (getSchemaName(qualifiedObjectName) == null)?getDefaultSchemaName():getSchemaName(qualifiedObjectName);
1873        if ((schema == null)||(schema.length() == 0)) schema = DEFAULT_SCHEMA_NAME;
1874
1875        // getCatalogSchemaNameWithDefault 这个函数有问题,为导致测试用例失败
1876        //        String[] catalogSchemaName = getCatalogSchemaNameWithDefault(qualifiedObjectName);
1877//        String db = catalogSchemaName[0];
1878//        String schema = catalogSchemaName[1];
1879
1880        TSQLCatalog sqlCatalog = createSQLCatalog(db);
1881        TSQLSchema sqlSchema = sqlCatalog.createSchema(schema);
1882        TSQLSchemaObject created = sqlSchema.createSchemaObject(getObjectName(qualifiedObjectName),objectType);
1883        // Ensure fast index contains the new object as well (already handled via putSchemaObject called by schema)
1884        return created;
1885    }
1886
1887    public TSQLSchemaObject doAddSchemaObject(TObjectName qualifiedObjectName,ESQLDataObjectType objectType){
1888        String db = (getDatabaseName(qualifiedObjectName) == null)?getDefaultCatalogName():getDatabaseName(qualifiedObjectName);
1889        if((db == null) || (db.length() == 0)) db = DEFAULT_DB_NAME;
1890        String schema = (getSchemaName(qualifiedObjectName) == null)?getDefaultSchemaName():getSchemaName(qualifiedObjectName);
1891        if ((schema == null)||(schema.length() == 0)) schema = DEFAULT_SCHEMA_NAME;
1892        TSQLCatalog sqlCatalog = createSQLCatalog(db);
1893        TSQLSchema sqlSchema = sqlCatalog.createSchema(schema);
1894        return sqlSchema.createSchemaObject(getObjectName(qualifiedObjectName),objectType);
1895    }
1896
1897    /**
1898     * Add a function to SQLEnv, if a function with the same already exists, just return the existing one.
1899     *
1900     * @param qualifiedFunctionName
1901     * @return function
1902     */
1903    public TSQLFunction addFunction(String qualifiedFunctionName, boolean fromDDL){
1904        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1905        return (TSQLFunction)doAddSchemaObject(qualifiedFunctionName,ESQLDataObjectType.dotFunction);
1906    }
1907
1908    public TSQLFunction addFunction(TObjectName qualifiedFunctionName, boolean fromDDL){
1909        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1910        return (TSQLFunction)doAddSchemaObject(qualifiedFunctionName,ESQLDataObjectType.dotFunction);
1911    }
1912
1913    public TSQLProcedure addOraclePackage(String qualifiedProcedureName, boolean fromDDL){
1914        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1915        return (TSQLProcedure)doAddSchemaObject(qualifiedProcedureName, ESQLDataObjectType.dotOraclePackage);
1916    }
1917
1918    public TSQLProcedure addProcedure(String qualifiedProcedureName, boolean fromDDL){
1919        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1920        return (TSQLProcedure)doAddSchemaObject(qualifiedProcedureName,ESQLDataObjectType.dotProcedure);
1921    }
1922
1923    public TSQLRoutine addSQLRoutine(String qualifiedProcedureName, boolean fromDDL, ESQLDataObjectType type){
1924        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1925        return (TSQLRoutine)doAddSchemaObject(qualifiedProcedureName,type);
1926    }
1927
1928    public TSQLTrigger addTrigger(String qualifiedTriggerName, boolean fromDDL){
1929        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1930        return (TSQLTrigger)doAddSchemaObject(qualifiedTriggerName,ESQLDataObjectType.dotTrigger);
1931    }
1932
1933    /**
1934     * Add a new table to the SQLEnv. If the table with the same name already exists in the SQLEnv,
1935     * the existing table will be returned.
1936     * <br>dbName.schemaName.tablename. If dbName is not specified, {@link #getDefaultCatalogName()} will be used to put this table in.
1937     * If schemaName is not specified, {@link #getDefaultSchemaName()} will be used to put this table in.
1938     * <br>If the specified database and schema are not already exists, a new database/schema will be generated automatically.
1939     *
1940     * @param qualifiedTableName qualified table name
1941     * @param fromDDL is this table generated from a create table statement
1942     *
1943     * @return SQL Table
1944     */
1945    public TSQLTable addTable(String qualifiedTableName, boolean fromDDL){
1946        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1947        return (TSQLTable)doAddSchemaObject(qualifiedTableName,ESQLDataObjectType.dotTable);
1948    }
1949
1950    public TSQLTable addView(String qualifiedViewName, boolean fromDDL){
1951        if ((fromDDL)&&(!isEnableGetMetadataFromDDL())) return  null;
1952        TSQLTable tsqlTable = (TSQLTable)doAddSchemaObject(qualifiedViewName,ESQLDataObjectType.dotTable);
1953        // doAddSchemaObject returns null on an object type/name conflict
1954        if (tsqlTable != null) {
1955            tsqlTable.setView(true);
1956        }
1957        return tsqlTable;
1958    }
1959
1960    public static String getObjectName(TObjectName schemaObjectName){
1961        if (schemaObjectName.getObjectString().length() == 0) return null;
1962        else
1963          return schemaObjectName.getObjectString();
1964    }
1965
1966    public static String getObjectName(String schemaObjectName){
1967        String[] names = SQLUtil.parseNames(schemaObjectName).toArray(new String[0]);
1968        return names[names.length-1];
1969    }
1970
1971    public static String getDatabaseName(TObjectName schemaObjectName){
1972        if (schemaObjectName.getDatabaseString().length() == 0)
1973            return null;
1974        else
1975           return schemaObjectName.getDatabaseString();
1976    }
1977
1978    public static String getDatabaseName(String schemaObjectName){
1979        String[] names = SQLUtil.parseNames(schemaObjectName).toArray(new String[0]);
1980        if (names.length == 3){
1981            return names[0];
1982        }else if (names.length == 4){
1983            return names[1];
1984        }else{
1985            return null;
1986        }
1987    }
1988
1989    public static String getSchemaName(TObjectName schemaObjectName){
1990        if (schemaObjectName.getSchemaString().length() == 0) return null;
1991        else
1992          return schemaObjectName.getSchemaString();
1993    }
1994
1995    public static String getSchemaName(String schemaObjectName){
1996        String[] names = SQLUtil.parseNames(schemaObjectName).toArray(new String[0]);
1997        if (names.length == 2){
1998            return names[0];
1999        }else if (names.length == 3){
2000            return names[1];
2001        }else if (names.length == 4){
2002            return names[2];
2003        }else{
2004            return null;
2005        }
2006    }
2007
2008    public TSQLFunction searchFunction(TObjectName qualifiedTablename) {
2009        TSQLSchemaObject result = searchSchemaObject(qualifiedTablename,dotFunction);
2010
2011        if (result instanceof TSQLFunction) {
2012            return (TSQLFunction) result;
2013        } else return null;
2014    }
2015
2016    public TSQLFunction searchFunction(String qualifiedTablename) {
2017        TSQLSchemaObject result = searchSchemaObject( getAFullQualifiedSchemaObjectName(qualifiedTablename),dotFunction);
2018
2019        if (result instanceof TSQLFunction) {
2020            return (TSQLFunction) result;
2021        } else return null;
2022    }
2023
2024    /**
2025     * If the input name is fully qualified with db and schema, just return it without any modification.
2026     * If the schema name is missed, use {@link #getDefaultSchemaName()} instead
2027     * If the database name is missed, use {@link #getDefaultCatalogName()} instead
2028     *
2029     * some sample result:
2030     * <br>
2031     * db.schema.table
2032     * .schema.table,  {@link #getDefaultCatalogName()} is null
2033     * ..table, both {@link #getDefaultCatalogName()} and {@link #getDefaultSchemaName()} are null.
2034     *
2035     * @param schemaObjectName
2036     * @return
2037     */
2038    public String getAFullQualifiedSchemaObjectName(String schemaObjectName){
2039        if (SQLUtil.parseNames(schemaObjectName).size() == 3) return schemaObjectName;
2040        String schema = (getDefaultSchemaName() == null)?"":getDefaultSchemaName();
2041        String db = (getDefaultCatalogName() == null)?"":getDefaultCatalogName();
2042
2043        String[] names = SQLUtil.parseNames(schemaObjectName).toArray(new String[0]);
2044        if (names.length == 1){ // no prefixed schema, db
2045            return db+"."+schema+"."+schemaObjectName;
2046        }else if (names.length == 2){ // prefix with schema, no database
2047            return db+"."+names[0]+"."+names[1];
2048        }else {
2049            return schemaObjectName;
2050        }
2051    }
2052
2053
2054
2055    public int getNumberOfTables(){
2056        int numOfTables = 0;
2057        for(int i=0;i<getCatalogList().size();i++){
2058            TSQLCatalog tsqlCatalog = getCatalogList().get(i);
2059            for(TSQLSchema tsqlSchema: tsqlCatalog.getSchemaList()){
2060                for(TSQLSchemaObject schemaObject: tsqlSchema.getSchemaObjectList()){
2061                    switch (schemaObject.getDataObjectType()){
2062                        case dotTable:
2063                            numOfTables++;
2064                            break;
2065                        case dotProcedure:
2066                            break;
2067                        case dotFunction:
2068                            break;
2069                        case dotOraclePackage:
2070                            break;
2071                    }
2072                } //schema object list
2073            } // schema list
2074        } //catalog list
2075        return numOfTables;
2076    }
2077
2078    public String toString(){
2079        String lcResult = "";
2080        StringBuilder tables = new StringBuilder();
2081        StringBuilder views = new StringBuilder();
2082        StringBuilder functions = new StringBuilder();
2083        StringBuilder oraclePackages = new StringBuilder();
2084        StringBuilder procedures = new StringBuilder();
2085        for(int i=0;i<getCatalogList().size();i++){
2086            TSQLCatalog tsqlCatalog = getCatalogList().get(i);
2087            lcResult = lcResult+"\ndatabase:"+tsqlCatalog.getName();
2088            for(TSQLSchema tsqlSchema: tsqlCatalog.getSchemaList()){
2089                lcResult = lcResult+"\n\t"+"schema:"+tsqlSchema.getName();
2090                for(TSQLSchemaObject schemaObject: tsqlSchema.getSchemaObjectList()){
2091                    switch (schemaObject.getDataObjectType()){
2092                        case dotTable:
2093                            TSQLTable tsqlTable = (TSQLTable)schemaObject;
2094                            if (tsqlTable.isView()){
2095                                views.append("\n\t\t\t"+schemaObject.getName());
2096                                for(TSQLColumn column: tsqlTable.getColumnList()){
2097                                    views.append("\n\t\t\t\t"+column.getName());
2098
2099                                }
2100                            }else{
2101                                tables.append("\n\t\t\t"+schemaObject.getName());
2102                                for(TSQLColumn column: tsqlTable.getColumnList()){
2103                                    tables.append("\n\t\t\t\t"+column.getName());
2104                                    if (column.getColumnDataType() != null){
2105                                        tables.append(",\t"+column.getColumnDataType().toString());
2106                                        TTypeName subType;
2107                                        switch (column.getColumnDataType().getDataType()){
2108                                            case array_t:
2109                                                subType = column.getColumnDataType().getTypeOfList();
2110
2111                                                if (subType.getDataType() == EDataType.struct_t){
2112
2113                                                    for(int k=0;k<subType.getColumnDefList().size();k++){
2114                                                        //System.out.println(subType.getColumnDefList().getColumn(k).getColumnName());
2115                                                        //System.out.println(subType.getColumnDefList().getColumn(k).getDatatype());
2116                                                        tables.append("\n\t\t\t\t\t\t"+ subType.getColumnDefList().getColumn(k).getColumnName().toString()
2117                                                                +",\t"+ subType.getColumnDefList().getColumn(k).getDatatype().toString());
2118                                                    }
2119                                                }
2120                                                break;
2121                                            case struct_t:
2122                                                subType = column.getColumnDataType();
2123                                                for(int k=0;k<subType.getColumnDefList().size();k++){
2124                                                    tables.append("\n\t\t\t\t\t\t"+ subType.getColumnDefList().getColumn(k).getColumnName().toString()
2125                                                            +",\t"+ subType.getColumnDefList().getColumn(k).getDatatype().toString());
2126                                                }
2127                                                break;
2128                                        }
2129                                    }
2130                                }
2131                            }
2132
2133                            break;
2134                        case dotProcedure:
2135                            procedures.append("\n\t\t\t"+schemaObject.getName());
2136                            break;
2137                        case dotFunction:
2138                            functions.append("\n\t\t\t"+schemaObject.toString());
2139                            //TSQLFunction f = (TSQLFunction) schemaObject;
2140
2141                            break;
2142                        case dotOraclePackage:
2143                            oraclePackages.append("\n\t\t\t"+schemaObject.getName());
2144                            break;
2145                    }
2146                } //schema object list
2147                lcResult = lcResult+"\n\t\t"+"tables:"+tables.toString();
2148                lcResult = lcResult+"\n\t\t"+"views:"+views.toString();
2149                if(oraclePackages.length()>0) {
2150                    lcResult = lcResult + "\n\t\t" + "oracle package:" + oraclePackages.toString();
2151                }
2152                lcResult = lcResult+"\n\t\t"+"procedure:"+procedures.toString();
2153                lcResult = lcResult+"\n\t\t"+"function:"+functions.toString();
2154                tables.setLength(0);
2155                views.setLength(0);
2156                procedures.setLength(0);
2157                functions.setLength(0);
2158            } // schema list
2159        } //catalog list
2160        return lcResult;
2161    }
2162
2163    /**
2164     * The text of the value preset for the parameter at position {@code paramPos} of the procedure (Mantis 4962):
2165     * the parameter's name comes from the procedure's parameter metadata, then {@link #getProcedureParameterValue(
2166     * String, String)}. Null when the procedure, its parameter metadata or the preset is missing, and for a preset
2167     * that is SQL NULL or a value supplied but not known.
2168     */
2169    public String getProcedureParameterValue(String procedureName, int paramPos){
2170        if (!presetsSupported()) {
2171            return null;
2172        }
2173        TSQLRoutine routine = findPresetRoutine(qualifyForPresets(procedureName, null, null));
2174        if (routine == null) {
2175            return null;
2176        }
2177        for (TSQLParameter parameter : routine.getParameterList()) {
2178            if (parameter.getParameterIndex() == paramPos && parameter.getName() != null) {
2179                TSQLRoutine.PresetValue preset = routine.getParameterPreset(parameter.getName());
2180                return preset == null ? null : preset.getText();
2181            }
2182        }
2183        return null;
2184    }
2185
2186    /**
2187     * Presets a value for parameter {@code parameterName} (with its {@code @}) of the procedure
2188     * {@code qualifiedProcedureName} (Mantis 4962): the dynamic SQL evaluation of the procedure reads it as the value
2189     * a caller passes, ahead of the calls of the procedure in the analysis (an explicit binding of
2190     * {@code resolveDynamicSqlLineage} replaces the presets). A name without a database or schema is in the default
2191     * catalog and schema, as {@link #addProcedure} places it: preset a procedure defined under {@code USE X} with
2192     * {@code X} in its name. The procedure is added when the catalog does not hold it yet. A null value removes the
2193     * preset.
2194     */
2195    public void setProcedureParameterValue(String qualifiedProcedureName, String parameterName,
2196            TSQLRoutine.PresetValue value) {
2197        requirePresetsSupported();
2198        if (qualifiedProcedureName == null || parameterName == null) {
2199            return;
2200        }
2201        String qualified = qualifyForPresets(qualifiedProcedureName, null, null);
2202        if (qualified == null) {
2203            return;
2204        }
2205        TSQLRoutine routine = findPresetRoutine(qualified);
2206        if (routine == null) {
2207            TSQLSchemaObject known = searchSchemaObject(qualified, ESQLDataObjectType.dotProcedure);
2208            routine = known instanceof TSQLRoutine ? (TSQLRoutine) known : null;
2209        }
2210        if (routine == null) {
2211            if (value == null) {
2212                return;
2213            }
2214            routine = addProcedure(qualified, false);
2215        }
2216        if (routine != null) {
2217            routine.setParameterPreset(parameterName, value);
2218        }
2219    }
2220
2221    /** {@link #setProcedureParameterValue(String, String, TSQLRoutine.PresetValue)} with a string value. */
2222    public void setProcedureParameterValue(String qualifiedProcedureName, String parameterName, String value) {
2223        setProcedureParameterValue(qualifiedProcedureName, parameterName, TSQLRoutine.PresetValue.string(value));
2224    }
2225
2226    /** The value preset for parameter {@code parameterName} of the procedure, or null when none is. */
2227    public TSQLRoutine.PresetValue getProcedureParameterValue(String qualifiedProcedureName, String parameterName) {
2228        requirePresetsSupported();
2229        TSQLRoutine routine = findPresetRoutine(qualifyForPresets(qualifiedProcedureName, null, null));
2230        return routine == null ? null : routine.getParameterPreset(parameterName);
2231    }
2232
2233    /** Every value preset for the procedure's parameters, by parameter name; empty when none is. */
2234    public Map<String, TSQLRoutine.PresetValue> getProcedureParameterPresets(String qualifiedProcedureName) {
2235        requirePresetsSupported();
2236        TSQLRoutine routine = findPresetRoutine(qualifyForPresets(qualifiedProcedureName, null, null));
2237        return routine == null ? new java.util.LinkedHashMap<String, TSQLRoutine.PresetValue>()
2238                : routine.getParameterPresets();
2239    }
2240
2241    /** True when some routine of the catalog has a preset: a lookup is needed only then. */
2242    public boolean hasParameterPresets() {
2243        return !presetRoutines.isEmpty();
2244    }
2245
2246    /**
2247     * The name {@code procedureName} stands for, as {@code database.schema.procedure}: a missing database is
2248     * {@code database}, else the default catalog, else {@link #DEFAULT_DB_NAME}; a missing schema is {@code schema},
2249     * else the default schema, else {@code dbo} (SQL Server's default schema). Setting and finding a preset use this
2250     * one rule, so a preset is found where it was put. Null for an empty name.
2251     */
2252    public String qualifyForPresets(String procedureName, String database, String schema) {
2253        if (procedureName == null) {
2254            return null;
2255        }
2256        List<String> parts = SQLUtil.parseNames(procedureName);
2257        if (parts.isEmpty()) {
2258            return null;
2259        }
2260        String name = parts.get(parts.size() - 1);
2261        String s = parts.size() >= 2 ? parts.get(parts.size() - 2) : null;
2262        if (s == null || s.length() == 0) {
2263            s = schema;
2264        }
2265        if ((s == null || s.length() == 0) && getDefaultSchemaName() != null && getDefaultSchemaName().length() > 0) {
2266            // the catalog's default is a stored name: written back as SQL, delimited
2267            s = IdentifierCodec.encodeStored(getIdentifierService().getProfile(), dotSchema, getDefaultSchemaName());
2268        }
2269        if (s == null || s.length() == 0) {
2270            s = "dbo";
2271        }
2272        String db = parts.size() >= 3 ? parts.get(parts.size() - 3) : null;
2273        if (db == null || db.length() == 0) {
2274            db = database;
2275        }
2276        if ((db == null || db.length() == 0) && getDefaultCatalogName() != null
2277                && getDefaultCatalogName().length() > 0) {
2278            db = IdentifierCodec.encodeStored(getIdentifierService().getProfile(), dotCatalog, getDefaultCatalogName());
2279        }
2280        if (db == null || db.length() == 0) {
2281            db = IdentifierCodec.encodeStored(getIdentifierService().getProfile(), dotCatalog, DEFAULT_DB_NAME);
2282        }
2283        return db + "." + s + "." + name;
2284    }
2285
2286    /** The routines that have presets, by identity: found without a catalog lookup that could fetch metadata. */
2287    private final java.util.Set<TSQLRoutine> presetRoutines = java.util.Collections.synchronizedSet(
2288            java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<TSQLRoutine, Boolean>()));
2289
2290    /** Called by a routine that gets a preset. */
2291    void registerPresetRoutine(TSQLRoutine routine) {
2292        presetRoutines.add(routine);
2293    }
2294
2295    /**
2296     * The routine with presets that {@code qualified} ({@code database.schema.procedure}) names, compared part by
2297     * part under this catalog's identifier rules; null when none. Reads only the routines that have presets, never
2298     * the catalog provider.
2299     */
2300    private TSQLRoutine findPresetRoutine(String qualified) {
2301        if (qualified == null || presetRoutines.isEmpty()) {
2302            return null;
2303        }
2304        List<String> parts = SQLUtil.parseNames(qualified);
2305        if (parts.size() < 3) {
2306            return null;
2307        }
2308        String db = parts.get(parts.size() - 3);
2309        String schema = parts.get(parts.size() - 2);
2310        String name = parts.get(parts.size() - 1);
2311        IdentifierService service = getIdentifierService();
2312        synchronized (presetRoutines) {
2313            for (TSQLRoutine routine : presetRoutines) {
2314                TSQLSchema s = routine.getSchema();
2315                TSQLCatalog c = s == null ? null : s.getCatalog();
2316                if (c != null && storedNames(service, routine.getName(), name, ESQLDataObjectType.dotProcedure)
2317                        && storedNames(service, s.getName(), schema, ESQLDataObjectType.dotSchema)
2318                        && storedNames(service, c.getName(), db, ESQLDataObjectType.dotCatalog)) {
2319                    return routine;
2320                }
2321            }
2322        }
2323        return null;
2324    }
2325
2326    /** True when the catalog's stored name {@code stored} is the name the SQL spelling {@code lexical} stands for. */
2327    private static boolean storedNames(IdentifierService service, String stored, String lexical,
2328            ESQLDataObjectType objectType) {
2329        CanonKey held = service.canonKeyOf(stored, objectType, IdentifierInputForm.CATALOG_STORED);
2330        return held != null && held.equals(service.canonKeyOf(lexical, objectType, IdentifierInputForm.SQL_LEXICAL));
2331    }
2332
2333    /** Parameter presets are read by the dynamic SQL evaluation of SQL Server procedures only. */
2334    private boolean presetsSupported() {
2335        return getDBVendor() == EDbVendor.dbvmssql || getDBVendor() == EDbVendor.dbvazuresql;
2336    }
2337
2338    private void requirePresetsSupported() {
2339        if (!presetsSupported()) {
2340            throw new UnsupportedOperationException("procedure parameter presets are read for SQL Server procedures"
2341                    + " only, not for " + getDBVendor());
2342        }
2343    }
2344
2345    public void getVariableValue(String variableName, String variableValue){}
2346
2347}