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 -> 属于 -> table1.column1 898 * 2. table1 -> 属于 -> db1.schema1.table1 899 * 3. `schema1.table1` -> 属于 -> `db1`.`schema1`.`table1` 900 * 4. `schema1.table1` -> 不属于 -> `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}