001package gudusoft.gsqlparser.sqlenv.parser.sharded; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 005import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 006import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 007import gudusoft.gsqlparser.sqlenv.TSQLEnv; 008import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 009import gudusoft.gsqlparser.sqlenv.TSQLSchema; 010import gudusoft.gsqlparser.sqlenv.TSQLTable; 011import gudusoft.gsqlparser.util.SQLUtil; 012import gudusoft.gsqlparser.util.json.JSON; 013 014import java.io.File; 015import java.io.IOException; 016import java.nio.charset.StandardCharsets; 017import java.nio.file.Files; 018import java.nio.file.Paths; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.logging.Level; 023import java.util.logging.Logger; 024 025/** 026 * SQLEnv implementation for sqlflow-sharded format. 027 * <p> 028 * Parses catalog.json files from the sharded export format, loading metadata 029 * including tables, views, columns, and synonyms. Supports different database 030 * topologies based on the manifest configuration. 031 * <p> 032 * The sharded format structure: 033 * <pre> 034 * manifest.json 035 * catalog/<shardId>.catalog.json 036 * source/<shardId>.source.jsonl 037 * index/<shardId>.source.idx 038 * </pre> 039 */ 040@SuppressWarnings("rawtypes") 041public class SqlflowShardedSQLEnv extends TSQLEnv { 042 043 private Map server; 044 private String baseDir; 045 private Boolean init = false; 046 private IdentifierInputForm nameForm = IdentifierInputForm.SQL_LEXICAL; 047 048 /** 049 * Constructor with explicit base directory for catalog files. 050 * 051 * @param defaultServer default server name 052 * @param defaultDatabase default database name 053 * @param defaultSchema default schema name 054 * @param dbVendor database vendor 055 * @param server server configuration from manifest 056 * @param baseDir base directory containing catalog/, source/, index/ subdirectories 057 */ 058 public SqlflowShardedSQLEnv(String defaultServer, String defaultDatabase, String defaultSchema, 059 EDbVendor dbVendor, Map server, String baseDir) { 060 this(defaultServer, defaultDatabase, defaultSchema, dbVendor, server, baseDir, 061 IdentifierInputForm.SQL_LEXICAL); 062 } 063 064 /** 065 * @param nameForm how the manifest and its catalog shards write object names: as SQL text 066 * ({@code "Amount"}, the exporter's form and the default) or as the names the 067 * database catalog holds ({@code Amount}). Declared once, at the manifest 068 * root, as {@code nameForm}. See {@link TSQLEnv#toLexical}. 069 */ 070 public SqlflowShardedSQLEnv(String defaultServer, String defaultDatabase, String defaultSchema, 071 EDbVendor dbVendor, Map server, String baseDir, IdentifierInputForm nameForm) { 072 this(defaultServer, defaultDatabase, defaultSchema, dbVendor, server, baseDir, nameForm, null); 073 } 074 075 /** 076 * Same input, analyzed under an explicit identifier configuration. 077 * 078 * <p>The metadata this environment registers is indexed with {@code identifierProfile}, so the 079 * analysis that reads it back asks the catalog the same question it was filled with. A profile 080 * belonging to another vendor is not applied ({@link IdentifierProfile#forVendorOr}); null means 081 * the vendor default. 082 */ 083 public SqlflowShardedSQLEnv(String defaultServer, String defaultDatabase, String defaultSchema, 084 EDbVendor dbVendor, Map server, String baseDir, IdentifierInputForm nameForm, 085 IdentifierProfile identifierProfile) { 086 super(dbVendor, IdentifierProfile.forVendorOr(identifierProfile, dbVendor)); 087 initDefaults(defaultServer, defaultDatabase, defaultSchema); 088 this.server = server; 089 this.baseDir = baseDir; 090 this.nameForm = nameForm == null ? IdentifierInputForm.SQL_LEXICAL : nameForm; 091 initSQLEnv(); 092 } 093 094 /** The spelling to register for a name from this input, per its declared {@link #nameForm}. */ 095 private String lex(ESQLDataObjectType type, String name) { 096 return TSQLEnv.toLexical(getDBVendor(), type, name, nameForm); 097 } 098 099 private void initDefaults(String defaultServer, String defaultDatabase, String defaultSchema) { 100 if (!SQLUtil.isEmpty(defaultServer) && !defaultServer.equals(TSQLEnv.DEFAULT_SERVER_NAME)) { 101 setDefaultServerName(defaultServer); 102 } 103 if (!SQLUtil.isEmpty(defaultDatabase) && !defaultDatabase.equals(TSQLEnv.DEFAULT_DB_NAME)) { 104 setDefaultCatalogName(defaultDatabase); 105 } 106 if (!SQLUtil.isEmpty(defaultSchema) && !defaultSchema.equals(TSQLEnv.DEFAULT_SCHEMA_NAME)) { 107 setDefaultSchemaName(defaultSchema); 108 } 109 } 110 111 @Override 112 public void initSQLEnv() { 113 synchronized (init) { 114 if (init) 115 return; 116 117 if (server == null) { 118 init = true; 119 return; 120 } 121 122 try { 123 // Parse dbVendor from server config 124 String dbVendor = (String) server.get("dbVendor"); 125 EDbVendor vendor = getDBVendor(); 126 if (dbVendor != null) { 127 vendor = EDbVendor.valueOf(dbVendor); 128 } 129 130 // Set server name from config 131 if (!SQLUtil.isEmpty((String) server.get("name"))) { 132 this.setDefaultServerName((String) server.get("name")); 133 } 134 135 boolean supportsCatalogs = TSQLEnv.supportCatalog(vendor); 136 boolean supportsSchemas = TSQLEnv.supportSchema(vendor); 137 138 // The manifest is authoritative about how THIS export was sharded, 139 // which may differ from the vendor's GSP defaults. Oracle defaults 140 // to catalog+schema, but its natural export is schema-only shards 141 // (servers[].schemas[]). Honor the declared flags so the correct 142 // process* branch is chosen; otherwise a schema-topology manifest 143 // routes to processDatabases(), finds no "databases", and loads 144 // nothing (silent empty env -> empty lineage). Mirror the same 145 // override already done in SqlflowShardedMetadataAnalyzer. 146 if (server.containsKey("supportsCatalogs")) { 147 supportsCatalogs = Boolean.TRUE.equals(server.get("supportsCatalogs")); 148 } 149 if (server.containsKey("supportsSchemas")) { 150 supportsSchemas = Boolean.TRUE.equals(server.get("supportsSchemas")); 151 } 152 153 // Get nameLevels for topology determination 154 List<String> nameLevels = getNameLevels(server); 155 156 // Resolve base directory 157 String resolvedBaseDir = resolveBaseDir(); 158 159 // List for pending synonyms (resolved after all tables are loaded) 160 List<Object[]> pendingSynonyms = new ArrayList<>(); 161 162 // Route on the PHYSICAL shard container first, not the vendor 163 // default. Schema-topology exports (Oracle) list shards under 164 // server.schemas[]; catalog-topology exports (SQL Server, MySQL) 165 // list them under server.databases[]. Only after the container 166 // is known do the catalog/schema flags decide how a databases[] 167 // shard's catalog file is interpreted. Deciding purely from the 168 // vendor default sent Oracle schema shards to processDatabases() 169 // (which reads databases[]) and loaded nothing; deciding purely 170 // from the flags would send a databases[]-wrapped schema-only 171 // manifest to processSchemas() (which reads schemas[]) and load 172 // nothing. Treat an empty list as absent. 173 List schemaShards = (List) server.get("schemas"); 174 List databaseShards = (List) server.get("databases"); 175 boolean hasSchemaShards = schemaShards != null && !schemaShards.isEmpty(); 176 boolean hasDatabaseShards = databaseShards != null && !databaseShards.isEmpty(); 177 178 if (hasSchemaShards) { 179 // Schema-topology shards (e.g., Oracle) 180 processSchemas(server, resolvedBaseDir, pendingSynonyms); 181 } else if (hasDatabaseShards) { 182 if (supportsCatalogs && supportsSchemas) { 183 // Catalog + Schema topology (e.g., SQL Server, PostgreSQL) 184 processDatabases(server, resolvedBaseDir, pendingSynonyms); 185 } else if (supportsCatalogs) { 186 // Catalog-only topology (e.g., MySQL) 187 processDatabasesAsCatalog(server, resolvedBaseDir, pendingSynonyms); 188 } else { 189 // Schema-only data wrapped in databases[]: the catalog 190 // file nests a schemas array; processDatabases already 191 // reads catalogJson.schemas (mirrors the metadata 192 // analyzer's processCatalogSchemaOnly). 193 processDatabases(server, resolvedBaseDir, pendingSynonyms); 194 } 195 } 196 197 // Resolve pending synonyms 198 for (Object[] pending : pendingSynonyms) { 199 CatalogMetadataHelper.appendSynonym((TSQLSchema) pending[0], (Map) pending[1], nameForm); 200 } 201 202 } catch (Exception e) { 203 Logger.getLogger(SqlflowShardedSQLEnv.class.getName()) 204 .log(Level.WARNING, "Parse sharded catalog failed", e); 205 } 206 207 init = true; 208 } 209 } 210 211 /** 212 * Get nameLevels from server config, defaulting based on vendor capabilities. 213 */ 214 private List<String> getNameLevels(Map server) { 215 List<String> nameLevels = (List<String>) server.get("nameLevels"); 216 if (nameLevels != null && !nameLevels.isEmpty()) { 217 return nameLevels; 218 } 219 220 // Default based on supportsCatalogs/supportsSchemas 221 Boolean supportsCatalogs = (Boolean) server.get("supportsCatalogs"); 222 Boolean supportsSchemas = (Boolean) server.get("supportsSchemas"); 223 224 List<String> defaultLevels = new ArrayList<>(); 225 if (supportsCatalogs != null && supportsCatalogs) { 226 defaultLevels.add("catalog"); 227 } 228 if (supportsSchemas != null && supportsSchemas) { 229 defaultLevels.add("schema"); 230 } 231 232 return defaultLevels; 233 } 234 235 /** 236 * Get base directory. 237 */ 238 private String resolveBaseDir() { 239 return baseDir; 240 } 241 242 /** 243 * Process databases in catalog+schema topology. 244 */ 245 private void processDatabases(Map server, String baseDir, List<Object[]> pendingSynonyms) { 246 List databases = (List) server.get("databases"); 247 if (databases == null) { 248 return; 249 } 250 251 for (int i = 0; i < databases.size(); i++) { 252 Map jsonDatabase = (Map) databases.get(i); 253 String databaseName = lex(ESQLDataObjectType.dotCatalog, (String) jsonDatabase.get("name")); 254 255 // Get catalog path 256 Map catalogInfo = (Map) jsonDatabase.get("catalog"); 257 if (catalogInfo == null) { 258 continue; 259 } 260 261 String catalogPath = (String) catalogInfo.get("path"); 262 if (SQLUtil.isEmpty(catalogPath)) { 263 continue; 264 } 265 266 // Resolve full path to catalog file 267 String fullCatalogPath = resolvePath(baseDir, catalogPath); 268 Map catalogJson = loadCatalogJson(fullCatalogPath); 269 if (catalogJson == null) { 270 continue; 271 } 272 273 // Process schemas within catalog 274 List schemas = (List) catalogJson.get("schemas"); 275 if (schemas == null) { 276 // Fallback: database might be directly in catalog root (nameLevels = ["database"]) 277 // In this case, create a default schema to hold tables/views 278 TSQLCatalog sqlCatalog = getSQLCatalog(databaseName, true); 279 TSQLSchema sqlSchema = sqlCatalog.getSchema(TSQLEnv.DEFAULT_SCHEMA_NAME, true); 280 appendTables(catalogJson, sqlSchema); 281 appendViews(catalogJson, sqlSchema); 282 CatalogMetadataHelper.appendPackages(catalogJson, sqlSchema, nameForm); 283 CatalogMetadataHelper.appendProcedures(catalogJson, sqlSchema, null, nameForm); 284 collectSynonyms(catalogJson, sqlSchema, pendingSynonyms); 285 } else { 286 TSQLCatalog sqlCatalog = getSQLCatalog(databaseName, true); 287 for (int j = 0; j < schemas.size(); j++) { 288 Map jsonSchema = (Map) schemas.get(j); 289 String schemaName = lex(ESQLDataObjectType.dotSchema, (String) jsonSchema.get("name")); 290 TSQLSchema sqlSchema = sqlCatalog.getSchema(schemaName, true); 291 appendTables(jsonSchema, sqlSchema); 292 appendViews(jsonSchema, sqlSchema); 293 CatalogMetadataHelper.appendPackages(jsonSchema, sqlSchema, nameForm); 294 CatalogMetadataHelper.appendProcedures(jsonSchema, sqlSchema, null, nameForm); 295 collectSynonyms(jsonSchema, sqlSchema, pendingSynonyms); 296 } 297 } 298 } 299 } 300 301 /** 302 * Process databases in catalog-only topology (no schema level). 303 */ 304 private void processDatabasesAsCatalog(Map server, String baseDir, List<Object[]> pendingSynonyms) { 305 List databases = (List) server.get("databases"); 306 if (databases == null) { 307 return; 308 } 309 310 for (int i = 0; i < databases.size(); i++) { 311 Map jsonDatabase = (Map) databases.get(i); 312 String databaseName = lex(ESQLDataObjectType.dotCatalog, (String) jsonDatabase.get("name")); 313 314 Map catalogInfo = (Map) jsonDatabase.get("catalog"); 315 if (catalogInfo == null) { 316 continue; 317 } 318 319 String catalogPath = (String) catalogInfo.get("path"); 320 if (SQLUtil.isEmpty(catalogPath)) { 321 continue; 322 } 323 324 String fullCatalogPath = resolvePath(baseDir, catalogPath); 325 Map catalogJson = loadCatalogJson(fullCatalogPath); 326 if (catalogJson == null) { 327 continue; 328 } 329 330 // In catalog-only mode, each database is treated as a catalog 331 // Tables/views are at the catalog level (not in schemas) 332 TSQLCatalog sqlCatalog = getSQLCatalog(databaseName, true); 333 TSQLSchema sqlSchema = sqlCatalog.getSchema(TSQLEnv.DEFAULT_SCHEMA_NAME, true); 334 335 // Tables/views might be at root level or in schemas 336 List tables = (List) catalogJson.get("tables"); 337 List views = (List) catalogJson.get("views"); 338 339 if (tables != null) { 340 for (Object table : tables) { 341 appendTableObject((Map) table, sqlSchema, false); 342 } 343 } 344 if (views != null) { 345 for (Object view : views) { 346 appendTableObject((Map) view, sqlSchema, true); 347 } 348 } 349 CatalogMetadataHelper.appendPackages(catalogJson, sqlSchema, nameForm); 350 CatalogMetadataHelper.appendProcedures(catalogJson, sqlSchema, null, nameForm); 351 } 352 } 353 354 /** 355 * Process schemas in schema-only topology (e.g., Oracle). 356 */ 357 private void processSchemas(Map server, String baseDir, List<Object[]> pendingSynonyms) { 358 List schemas = (List) server.get("schemas"); 359 if (schemas == null) { 360 return; 361 } 362 363 for (int i = 0; i < schemas.size(); i++) { 364 Map jsonSchema = (Map) schemas.get(i); 365 String schemaName = lex(ESQLDataObjectType.dotSchema, (String) jsonSchema.get("name")); 366 367 // Get catalog path 368 Map catalogInfo = (Map) jsonSchema.get("catalog"); 369 String fullCatalogPath = null; 370 Map catalogJson = null; 371 372 if (catalogInfo != null) { 373 String catalogPath = (String) catalogInfo.get("path"); 374 if (!SQLUtil.isEmpty(catalogPath)) { 375 fullCatalogPath = resolvePath(baseDir, catalogPath); 376 catalogJson = loadCatalogJson(fullCatalogPath); 377 } 378 } 379 380 // Create synthetic catalog for schema-only vendors 381 TSQLCatalog sqlCatalog = getSQLCatalog(TSQLEnv.DEFAULT_DB_NAME, true); 382 TSQLSchema sqlSchema = sqlCatalog.getSchema(schemaName, true); 383 384 if (catalogJson != null) { 385 appendTables(catalogJson, sqlSchema); 386 appendViews(catalogJson, sqlSchema); 387 CatalogMetadataHelper.appendPackages(catalogJson, sqlSchema, nameForm); 388 CatalogMetadataHelper.appendProcedures(catalogJson, sqlSchema, null, nameForm); 389 collectSynonyms(catalogJson, sqlSchema, pendingSynonyms); 390 } else { 391 // Process directly from schema config 392 appendTables(jsonSchema, sqlSchema); 393 appendViews(jsonSchema, sqlSchema); 394 CatalogMetadataHelper.appendPackages(jsonSchema, sqlSchema, nameForm); 395 CatalogMetadataHelper.appendProcedures(jsonSchema, sqlSchema, null, nameForm); 396 collectSynonyms(jsonSchema, sqlSchema, pendingSynonyms); 397 } 398 } 399 } 400 401 /** 402 * Load and parse a catalog JSON file. 403 */ 404 private Map loadCatalogJson(String path) { 405 if (path == null) { 406 return null; // unresolvable relative path with no base directory 407 } 408 try { 409 File file = new File(path); 410 if (!file.exists()) { 411 Logger.getLogger(SqlflowShardedSQLEnv.class.getName()) 412 .warning("Catalog file not found: " + path); 413 return null; 414 } 415 416 String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); 417 Object parsed = JSON.parseObject(content); 418 if (parsed instanceof Map) { 419 return (Map) parsed; 420 } 421 } catch (IOException e) { 422 Logger.getLogger(SqlflowShardedSQLEnv.class.getName()) 423 .log(Level.WARNING, "Failed to read catalog file: " + path, e); 424 } 425 return null; 426 } 427 428 /** 429 * Resolve relative path against base directory. 430 */ 431 private String resolvePath(String baseDir, String relativePath) { 432 // An absolute path stands on its own regardless of base directory. 433 if (new File(relativePath).isAbsolute()) { 434 return relativePath; 435 } 436 if (baseDir == null) { 437 // Do NOT resolve a relative shard path against the process working 438 // directory: that could read a same-named catalog from CWD and merge 439 // a wrong environment. Without a base directory the shard is simply 440 // unreachable (no catalog is better than a wrong one). 441 return null; 442 } 443 444 // Handle both forward slash and backslash 445 String normalizedBase = baseDir.replace('\\', '/'); 446 String normalizedRelative = relativePath.replace('\\', '/'); 447 448 return Paths.get(normalizedBase, normalizedRelative).toString(); 449 } 450 451 /** 452 * Append tables from a schema/catalog JSON object. 453 */ 454 private void appendTables(Map jsonSchema, TSQLSchema sqlSchema) { 455 List tables = (List) jsonSchema.get("tables"); 456 if (tables == null) { 457 return; 458 } 459 460 for (int i = 0; i < tables.size(); i++) { 461 Map jsonTable = (Map) tables.get(i); 462 appendTableObject(jsonTable, sqlSchema, false); 463 } 464 } 465 466 /** 467 * Append views from a schema/catalog JSON object. 468 */ 469 private void appendViews(Map jsonSchema, TSQLSchema sqlSchema) { 470 List views = (List) jsonSchema.get("views"); 471 if (views == null) { 472 return; 473 } 474 475 for (int i = 0; i < views.size(); i++) { 476 Map jsonTable = (Map) views.get(i); 477 appendTableObject(jsonTable, sqlSchema, true); 478 } 479 } 480 481 /** 482 * Append a single table or view object. 483 */ 484 private void appendTableObject(Map jsonTable, TSQLSchema sqlSchema, boolean isView) { 485 String tableName = lex(ESQLDataObjectType.dotTable, (String) jsonTable.get("name")); 486 if (SQLUtil.isEmpty(tableName)) { 487 return; 488 } 489 490 TSQLTable sqlTable = sqlSchema.createTable(tableName, isView ? 3 : 0); 491 if (isView) { 492 sqlTable.setView(true); 493 } 494 495 // Set table type if present 496 String type = (String) jsonTable.get("type"); 497 if (type != null && type.toLowerCase(java.util.Locale.ROOT).indexOf("view") != -1) { 498 sqlTable.setView(true); 499 } 500 501 // Add columns 502 List columns = (List) jsonTable.get("columns"); 503 if (columns != null) { 504 for (int j = 0; j < columns.size(); j++) { 505 Map jsonColumn = (Map) columns.get(j); 506 String columnName = lex(ESQLDataObjectType.dotColumn, (String) jsonColumn.get("name")); 507 if (!SQLUtil.isEmpty(columnName)) { 508 sqlTable.addColumn(columnName); 509 } 510 } 511 } 512 } 513 514 /** 515 * Collect synonyms for later resolution. 516 */ 517 private void collectSynonyms(Map jsonSchema, TSQLSchema sqlSchema, List<Object[]> pendingSynonyms) { 518 List synonyms = (List) jsonSchema.get("synonyms"); 519 if (synonyms == null) { 520 return; 521 } 522 523 for (int i = 0; i < synonyms.size(); i++) { 524 Map jsonSynonym = (Map) synonyms.get(i); 525 if (jsonSynonym == null) { 526 continue; 527 } 528 pendingSynonyms.add(new Object[]{sqlSchema, jsonSynonym}); 529 } 530 } 531}