001package gudusoft.gsqlparser.util; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TGSqlParser; 005import gudusoft.gsqlparser.util.json.JSON; 006 007import java.io.*; 008import java.util.ArrayList; 009import java.util.LinkedHashMap; 010import java.util.List; 011import java.util.Map; 012 013public class FileSplitter { 014 015 private static final int MAX_CONSECUTIVE_FAILURES = 3; 016 private static final int BUFFER_SIZE = 8192; 017 private static final Logger logger = LoggerFactory.getLogger(FileSplitter.class); 018 019 /** 020 * Split large file 021 * 022 * @param inputFile Input file 023 * @param outputDir Output directory 024 * @param splitSizeMB Split size (MB) 025 * @param dbVendor Database vendor 026 * @return List of split files 027 * @throws IOException IO exception 028 */ 029 public static List<File> splitFile(File inputFile, File outputDir, int splitSizeMB, EDbVendor dbVendor) throws IOException { 030 List<File> splitFiles = new ArrayList<>(); 031 if (!inputFile.exists()) { 032 throw new FileNotFoundException("Input file does not exist: " + inputFile.getAbsolutePath()); 033 } 034 035 if (!outputDir.exists() && !outputDir.mkdirs()) { 036 throw new IOException("Failed to create output directory: " + outputDir.getAbsolutePath()); 037 } 038 039 // Check if it's a JSON file 040 if (isJsonFile(inputFile)) { 041 return splitJsonFile(inputFile, outputDir, splitSizeMB); 042 } 043 044 // Process regular SQL file 045 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 046 047 try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r")) { 048 long fileLength = raf.length(); 049 long currentPos = 0; 050 long chunkStartPos = 0; 051 int consecutiveFailures = 0; 052 int fileIndex = 1; 053 long globalStartLineNo = 1; 054 055 while (currentPos < fileLength) { 056 Long lastValidatedLineNo = getLastLineFromCurrentPos(raf, currentPos, fileLength, splitSizeBytes, dbVendor); 057 if (lastValidatedLineNo == null) break; 058 059 while (lastValidatedLineNo != null && lastValidatedLineNo == -1) { 060 consecutiveFailures++; 061 logger.warn("No validated statement found at position " + currentPos + " (consecutive failures: " + consecutiveFailures + ")"); 062 063 long incrementSize = Math.min(splitSizeBytes, fileLength - currentPos); 064 long previousPos = currentPos; 065 currentPos = Math.min(currentPos + incrementSize, fileLength); 066 067 if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { 068 logger.error("Reached maximum consecutive failures (" + MAX_CONSECUTIVE_FAILURES + "), skipping this segment and moving to next"); 069 long skipSize = splitSizeBytes * (consecutiveFailures + 1); 070 currentPos = Math.min(previousPos + skipSize, fileLength); 071 chunkStartPos = currentPos; 072 consecutiveFailures = 0; // Reset consecutive failure counter 073 logger.info("Skipped to position " + currentPos + ", continuing with next segment"); 074 break; 075 } 076 077 lastValidatedLineNo = getLastLineFromCurrentPos(raf, currentPos, fileLength, splitSizeBytes * (consecutiveFailures + 1), dbVendor); 078 } 079 080 if (lastValidatedLineNo == null) { 081 break; 082 } 083 084 if (lastValidatedLineNo == -1) { 085 continue; 086 } 087 088 consecutiveFailures = 0; 089 long endPos = findLineEndPosition(raf, currentPos, lastValidatedLineNo); 090 long globalEndLineNo = globalStartLineNo + lastValidatedLineNo - 1; 091 092 File splitFile = splitFileByPosition(outputDir, inputFile, chunkStartPos, endPos, fileIndex, globalStartLineNo, globalEndLineNo); 093 splitFiles.add(splitFile); 094 095 chunkStartPos = endPos; 096 currentPos = endPos; 097 globalStartLineNo = globalEndLineNo + 1; 098 fileIndex++; 099 100 if (currentPos >= fileLength) { 101 break; 102 } 103 } 104 } 105 106 return splitFiles; 107 } 108 109 /** 110 * Check if file is a JSON file 111 */ 112 private static boolean isJsonFile(File file) { 113 try (BufferedReader reader = new BufferedReader(new FileReader(file))) { 114 int c; 115 // Skip whitespace characters 116 while ((c = reader.read()) != -1) { 117 if (!Character.isWhitespace(c)) { 118 // Check if first non-whitespace character is '{' 119 return c == '{'; 120 } 121 } 122 } catch (IOException e) { 123 logger.error("Error reading file to check if it's JSON: " + file.getAbsolutePath(), e); 124 } 125 return false; 126 } 127 128 /** 129 * Get file extension 130 */ 131 private static String getFileExtension(String fileName) { 132 int lastDotIndex = fileName.lastIndexOf('.'); 133 if (lastDotIndex == -1) { 134 return ""; 135 } 136 return fileName.substring(lastDotIndex + 1); 137 } 138 139 /** 140 * Get file name without extension 141 */ 142 private static String getFileNameWithoutExtension(String fileName) { 143 int lastDotIndex = fileName.lastIndexOf('.'); 144 if (lastDotIndex == -1) { 145 return fileName; 146 } 147 return fileName.substring(0, lastDotIndex); 148 } 149 150 /** 151 * Move file 152 */ 153 private static void moveFile(File source, File target) throws IOException { 154 if (!source.exists()) { 155 throw new FileNotFoundException("Source file does not exist: " + source.getAbsolutePath()); 156 } 157 if (target.exists() && !target.delete()) { 158 throw new IOException("Failed to delete existing target file: " + target.getAbsolutePath()); 159 } 160 if (!source.renameTo(target)) { 161 // If renameTo fails, try copy and delete 162 copyFile(source, target); 163 if (!source.delete()) { 164 throw new IOException("Failed to delete source file after copy: " + source.getAbsolutePath()); 165 } 166 } 167 } 168 169 /** 170 * Copy file 171 */ 172 private static void copyFile(File source, File target) throws IOException { 173 try (FileInputStream fis = new FileInputStream(source); 174 FileOutputStream fos = new FileOutputStream(target)) { 175 byte[] buffer = new byte[BUFFER_SIZE]; 176 int bytesRead; 177 while ((bytesRead = fis.read(buffer)) != -1) { 178 fos.write(buffer, 0, bytesRead); 179 } 180 } 181 } 182 183 /** 184 * Split JSON file 185 */ 186 private static List<File> splitJsonFile(File inputFile, File outputDir, int splitSizeMB) throws IOException { 187 List<File> splitFiles = new ArrayList<>(); 188 189 // Read file content 190 String jsonContent = readFileContent(inputFile); 191 Map<?, ?> content = (Map<?, ?>) JSON.parseObject(jsonContent); 192 String createdBy = (String) content.get("createdBy"); 193 String format = (String) content.get("format"); 194 195 if (createdBy == null) { 196 logger.warn("JSON file does not contain 'createdBy' field, treating as regular file"); 197 return splitFiles; 198 } 199 200 // Check for sqlflow-sharded format (new version) 201 if ("sqlflow-sharded".equals(format)) { 202 splitFiles.addAll(splitSqlflowShardedJson(inputFile, outputDir, splitSizeMB, content)); 203 } else if (createdBy.toLowerCase().contains("sqldep") || createdBy.toLowerCase().contains("grabit")) { 204 splitFiles.addAll(splitSqldepGrabitJson(inputFile, outputDir, splitSizeMB, content, createdBy)); 205 } else if (createdBy.toLowerCase().contains("sqlflow")) { 206 splitFiles.addAll(splitSqlflowJson(inputFile, outputDir, splitSizeMB, content)); 207 } else { 208 logger.warn("Unknown JSON file type with createdBy: " + createdBy); 209 } 210 211 return splitFiles; 212 } 213 214 private static String readFileContent(File inputFile) { 215 return SQLUtil.getFileContent(inputFile); 216 } 217 218 /** 219 * Write JSON file 220 */ 221 private static void writeJsonFile(File file, Object content) throws IOException { 222 String jsonString = JSON.toJSONString(content); 223 SQLUtil.writeToFile(file, jsonString); 224 } 225 226 /** 227 * Split sqldep or grabit format JSON file 228 */ 229 private static List<File> splitSqldepGrabitJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> content, String createdBy) throws IOException { 230 List<File> splitFiles = new ArrayList<>(); 231 List<Map<?, ?>> queries = (List<Map<?, ?>>) content.get("queries"); 232 if (queries == null || queries.isEmpty()) { 233 logger.warn("No queries found in JSON file"); 234 return splitFiles; 235 } 236 237 // Remove queries field, create metadata file 238 @SuppressWarnings("unchecked") 239 Map<String, Object> metadataContent = new LinkedHashMap<>((Map<String, Object>) content); 240 metadataContent.remove("queries"); 241 242 String extension = getFileExtension(inputFile.getName()); 243 File metadataFile = new File(outputDir, getFileNameWithoutExtension(inputFile.getName()) + "_" + 0 + "_" + 1 + (extension.isEmpty() ? "" : "." + extension)); 244 writeJsonFile(metadataFile, metadataContent); 245 splitFiles.add(metadataFile); 246 logger.info("Created metadata file: " + metadataFile.getAbsolutePath()); 247 248 // Split queries 249 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 250 int length = 0; 251 List<Map<?, ?>> temp = new ArrayList<>(); 252 long startIndex = 1; 253 long endIndex = 1; 254 255 for (Map<?, ?> item : queries) { 256 temp.add(item); 257 String sourceCode = (String) item.get("sourceCode"); 258 if (sourceCode != null) { 259 length += sourceCode.length(); 260 } 261 262 if (length >= splitSizeBytes) { 263 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 264 Map<String, Object> jsonObject = new LinkedHashMap<>(); 265 jsonObject.put("createdBy", createdBy); 266 jsonObject.put("dbvendor", content.get("dbvendor")); 267 jsonObject.put("databases", new ArrayList<>()); 268 jsonObject.put("queries", temp); 269 writeJsonFile(queryFile, jsonObject); 270 splitFiles.add(queryFile); 271 logger.info("Created query file: " + queryFile.getAbsolutePath()); 272 273 temp.clear(); 274 length = 0; 275 startIndex = endIndex; 276 } 277 endIndex++; 278 } 279 280 // Process remaining queries 281 if (!temp.isEmpty()) { 282 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 283 Map<String, Object> jsonObject = new LinkedHashMap<>(); 284 jsonObject.put("createdBy", createdBy); 285 jsonObject.put("dbvendor", content.get("dbvendor")); 286 jsonObject.put("databases", new ArrayList<>()); 287 jsonObject.put("queries", temp); 288 writeJsonFile(queryFile, jsonObject); 289 splitFiles.add(queryFile); 290 logger.info("Created query file: " + queryFile.getAbsolutePath()); 291 } 292 293 return splitFiles; 294 } 295 296 /** 297 * Split sqlflow format JSON file 298 */ 299 private static List<File> splitSqlflowJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> content) throws IOException { 300 List<File> splitFiles = new ArrayList<>(); 301 List<Map<?, ?>> queries = new ArrayList<>(); 302 303 // Extract all queries 304 List<Map<?, ?>> servers = (List<Map<?, ?>>) content.get("servers"); 305 if (servers != null) { 306 for (Map<?, ?> serverObject : servers) { 307 @SuppressWarnings("unchecked") 308 List<Map<?, ?>> serverQueries = (List<Map<?, ?>>) serverObject.get("queries"); 309 if (serverQueries != null) { 310 queries.addAll(serverQueries); 311 serverObject.remove("queries"); 312 } 313 } 314 } 315 316 if (queries.isEmpty()) { 317 logger.warn("No queries found in SQLFlow JSON file"); 318 return splitFiles; 319 } 320 321 // Create metadata file 322 String extension = getFileExtension(inputFile.getName()); 323 File metadataFile = new File(outputDir, getFileNameWithoutExtension(inputFile.getName()) + "_" + 0 + "_" + 1 + (extension.isEmpty() ? "" : "." + extension)); 324 writeJsonFile(metadataFile, content); 325 splitFiles.add(metadataFile); 326 logger.info("Created metadata file: " + metadataFile.getAbsolutePath()); 327 328 // Split queries 329 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 330 int length = 0; 331 List<Map<?, ?>> temp = new ArrayList<>(); 332 long startIndex = 1; 333 long endIndex = 1; 334 335 for (Map<?, ?> item : queries) { 336 temp.add(item); 337 String sourceCode = (String) item.get("sourceCode"); 338 if (sourceCode != null) { 339 length += sourceCode.length(); 340 } 341 342 if (length >= splitSizeBytes) { 343 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 344 Map<String, Object> jsonObject = new LinkedHashMap<>(); 345 jsonObject.put("createdBy", "grabit v1.7.0"); 346 jsonObject.put("databases", new ArrayList<>()); 347 jsonObject.put("queries", temp); 348 writeJsonFile(queryFile, jsonObject); 349 splitFiles.add(queryFile); 350 logger.info("Created query file: " + queryFile.getAbsolutePath()); 351 352 temp.clear(); 353 length = 0; 354 startIndex = endIndex; 355 } 356 endIndex++; 357 } 358 359 // Process remaining queries 360 if (!temp.isEmpty()) { 361 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 362 Map<String, Object> jsonObject = new LinkedHashMap<>(); 363 jsonObject.put("createdBy", "grabit v1.7.0"); 364 jsonObject.put("databases", new ArrayList<>()); 365 jsonObject.put("queries", temp); 366 writeJsonFile(queryFile, jsonObject); 367 splitFiles.add(queryFile); 368 logger.info("Created query file: " + queryFile.getAbsolutePath()); 369 } 370 371 return splitFiles; 372 } 373 374 //autogenerate 375 /** 376 * Split sqlflow-sharded format JSON file. 377 * Convert sqlflow-sharded to legacy sqlflow format: 378 * - catalog: if single db > splitSizeMB, keep as one file; else merge with next db until >= splitSizeMB 379 * - source: if single db < splitSizeMB, merge with next db; if > splitSizeMB, split by size keeping records complete 380 */ 381 private static List<File> splitSqlflowShardedJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> manifest) throws IOException { 382 List<File> splitFiles = new ArrayList<>(); 383 384 File baseDir = inputFile.getParentFile(); 385 if (baseDir == null) { 386 logger.warn("Cannot determine base directory from manifest file"); 387 return splitFiles; 388 } 389 390 String createdBy = (String) manifest.get("createdBy"); 391 List<Map<?, ?>> servers = (List<Map<?, ?>>) manifest.get("servers"); 392 if (servers == null || servers.isEmpty()) { 393 logger.warn("No servers found in sqlflow-sharded manifest"); 394 return splitFiles; 395 } 396 397 String extension = getFileExtension(inputFile.getName()); 398 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 399 400 // Collect all database info first 401 List<DatabaseCatalogInfo> allCatalogs = new ArrayList<>(); 402 List<DatabaseSourceInfo> allSources = new ArrayList<>(); 403 404 for (Map<?, ?> server : servers) { 405 String serverName = (String) server.get("name"); 406 String dbVendor = (String) server.get("dbVendor"); 407 408 List<Map<?, ?>> databases = (List<Map<?, ?>>) server.get("databases"); 409 if (databases == null || databases.isEmpty()) { 410 continue; 411 } 412 413 for (Map<?, ?> database : databases) { 414 String databaseName = (String) database.get("name"); 415 String shardId = (String) database.get("shardId"); 416 417 // Collect catalog info 418 Map<?, ?> catalogInfo = (Map<?, ?>) database.get("catalog"); 419 if (catalogInfo != null) { 420 String catalogPath = (String) catalogInfo.get("path"); 421 File catalogFile = resolvePath(baseDir, catalogPath); 422 if (catalogFile != null && catalogFile.exists()) { 423 allCatalogs.add(new DatabaseCatalogInfo(serverName, dbVendor, databaseName, shardId, catalogFile)); 424 } 425 } 426 427 // Collect source info 428 Map<?, ?> sourceInfo = (Map<?, ?>) database.get("source"); 429 String sourceCompression = (String) manifest.get("sourceCompression"); 430 if (sourceInfo != null) { 431 String sourcePath = (String) sourceInfo.get("path"); 432 File sourceFile = resolvePath(baseDir, sourcePath); 433 if (sourceFile != null && sourceFile.exists()) { 434 allSources.add(new DatabaseSourceInfo(serverName, dbVendor, databaseName, shardId, sourceFile, sourceCompression)); 435 } 436 } 437 } 438 } 439 440 // Process catalogs: merge if total < splitSizeMB, otherwise keep single 441 List<File> catalogFiles = processCatalogs(outputDir, inputFile, extension, allCatalogs, splitSizeBytes, createdBy); 442 splitFiles.addAll(catalogFiles); 443 444 // Process sources: merge if total < splitSizeMB, split by size if > splitSizeMB 445 List<File> sourceFiles = processSources(outputDir, inputFile, extension, allSources, splitSizeBytes, createdBy); 446 splitFiles.addAll(sourceFiles); 447 448 return splitFiles; 449 } 450 451 /** 452 * Process catalogs: merge if total size < 2*splitSizeMB, otherwise keep single 453 */ 454 private static List<File> processCatalogs(File outputDir, File inputFile, String extension, 455 List<DatabaseCatalogInfo> catalogs, long splitSizeBytes, 456 String createdBy) throws IOException { 457 List<File> result = new ArrayList<>(); 458 459 if (catalogs == null || catalogs.isEmpty()) { 460 return result; 461 } 462 463 int catalogIndex = 1; 464 List<DatabaseCatalogInfo> batch = new ArrayList<>(); 465 int currentSize = 0; 466 long maxBatchSize = splitSizeBytes * 2; 467 468 for (DatabaseCatalogInfo catalog : catalogs) { 469 int catalogSize = (int) catalog.file.length(); 470 471 // If single catalog > splitSizeBytes (5MB), it must be alone 472 if (catalogSize > splitSizeBytes) { 473 // Save current batch first 474 if (!batch.isEmpty()) { 475 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 476 if (batchFile != null) { 477 result.add(batchFile); 478 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 479 } 480 catalogIndex++; 481 batch = new ArrayList<>(); 482 currentSize = 0; 483 } 484 // Write this large catalog alone 485 File singleFile = createCatalogSingleFile(outputDir, inputFile, catalog, extension, catalogIndex, createdBy); 486 if (singleFile != null) { 487 result.add(singleFile); 488 logger.info("Created catalog file: " + singleFile.getAbsolutePath()); 489 } 490 catalogIndex++; 491 } else { 492 // Check if adding this catalog would exceed 2*splitSize (10MB) 493 if (currentSize + catalogSize > maxBatchSize && !batch.isEmpty()) { 494 // Save current batch 495 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 496 if (batchFile != null) { 497 result.add(batchFile); 498 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 499 } 500 catalogIndex++; 501 batch = new ArrayList<>(); 502 currentSize = 0; 503 } 504 batch.add(catalog); 505 currentSize += catalogSize; 506 } 507 } 508 509 // Save remaining batch 510 if (!batch.isEmpty()) { 511 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 512 if (batchFile != null) { 513 result.add(batchFile); 514 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 515 } 516 } 517 518 return result; 519 } 520 521 /** 522 * Process sources: accumulate all records, split by size > splitSizeMB 523 */ 524 private static List<File> processSources(File outputDir, File inputFile, String extension, 525 List<DatabaseSourceInfo> sources, long splitSizeBytes, 526 String createdBy) throws IOException { 527 List<File> result = new ArrayList<>(); 528 529 if (sources == null || sources.isEmpty()) { 530 return result; 531 } 532 533 int startIndex = 1; 534 List<Map<String, Object>> currentBatch = new ArrayList<>(); 535 int currentSize = 0; 536 List<String> mergedFrom = new ArrayList<>(); 537 538 for (DatabaseSourceInfo source : sources) { 539 mergedFrom.add(source.shardId); 540 541 List<Map<?, ?>> records = readSourceRecords(source.file, source.compression); 542 if (records == null || records.isEmpty()) { 543 continue; 544 } 545 546 for (Map<?, ?> record : records) { 547 Map<String, Object> query = new LinkedHashMap<>(); 548 query.put("database", source.databaseName); 549 query.put("schema", record.get("schema")); 550 query.put("name", record.get("name")); 551 query.put("type", record.get("type")); 552 query.put("sourceCode", record.get("sourceCode")); 553 if (record.containsKey("groupName")) { 554 query.put("groupName", record.get("groupName")); 555 } 556 557 Object sourceCode = record.get("sourceCode"); 558 int recordSize = sourceCode != null ? sourceCode.toString().length() : 0; 559 560 if (currentSize + recordSize > splitSizeBytes && !currentBatch.isEmpty()) { 561 int endIndex = startIndex + currentBatch.size() - 1; 562 File batchFile = createSourceQueryFileCommon(outputDir, inputFile, extension, 563 currentBatch, mergedFrom, startIndex, endIndex, createdBy); 564 if (batchFile != null) { 565 result.add(batchFile); 566 logger.info("Created source batch file: " + batchFile.getAbsolutePath()); 567 } 568 startIndex = endIndex + 1; 569 currentBatch = new ArrayList<>(); 570 currentSize = 0; 571 } 572 573 currentBatch.add(query); 574 currentSize += recordSize; 575 } 576 } 577 578 // Save remaining batch 579 if (!currentBatch.isEmpty()) { 580 int endIndex = startIndex + currentBatch.size() - 1; 581 File batchFile = createSourceQueryFileCommon(outputDir, inputFile, extension, 582 currentBatch, mergedFrom, startIndex, endIndex, createdBy); 583 if (batchFile != null) { 584 result.add(batchFile); 585 logger.info("Created source batch file: " + batchFile.getAbsolutePath()); 586 } 587 } 588 589 return result; 590 } 591 592 /** 593 * Calculate total size of source records 594 */ 595 private static int calculateSourceSize(List<Map<?, ?>> records) { 596 if (records == null) return 0; 597 int size = 0; 598 for (Map<?, ?> record : records) { 599 Object sourceCode = record.get("sourceCode"); 600 if (sourceCode != null) { 601 size += sourceCode.toString().length(); 602 } 603 } 604 return size; 605 } 606 607 /** 608 * Create catalog file for a batch of databases 609 */ 610 private static File createCatalogBatchFile(File outputDir, File inputFile, String extension, 611 List<DatabaseCatalogInfo> batch, int batchIndex, 612 String createdBy) throws IOException { 613 if (batch == null || batch.isEmpty()) { 614 return null; 615 } 616 617 Map<String, Object> sqlflow = new LinkedHashMap<>(); 618 sqlflow.put("createdBy", createdBy != null ? createdBy : "sqlflow-ingester"); 619 sqlflow.put("format", "sqlflow"); 620 621 // Add mergedFrom field to track original shardIds 622 List<String> mergedFrom = new ArrayList<>(); 623 624 List<Map<String, Object>> servers = new ArrayList<>(); 625 626 for (DatabaseCatalogInfo catalog : batch) { 627 mergedFrom.add(catalog.shardId); 628 629 Map<String, Object> catalogContent = readCatalogContent(catalog.file); 630 if (catalogContent == null) continue; 631 632 Map<String, Object> serverObj = new LinkedHashMap<>(); 633 serverObj.put("name", catalog.serverName); 634 serverObj.put("dbVendor", catalog.dbVendor); 635 636 Map<String, Object> dbObj = new LinkedHashMap<>(); 637 dbObj.put("name", catalog.databaseName); 638 639 copyCatalogFields(catalogContent, dbObj); 640 641 List<Map<String, Object>> databases = new ArrayList<>(); 642 databases.add(dbObj); 643 serverObj.put("databases", databases); 644 servers.add(serverObj); 645 } 646 647 sqlflow.put("servers", servers); 648 sqlflow.put("databases", new ArrayList<>()); 649 sqlflow.put("mergedFrom", mergedFrom); 650 651 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_catalog_" + batchIndex + 652 (extension.isEmpty() ? "" : "." + extension); 653 654 File outputFile = new File(outputDir, fileName); 655 writeJsonFile(outputFile, sqlflow); 656 return outputFile; 657 } 658 659 /** 660 * Create catalog file for a single large database 661 */ 662 private static File createCatalogSingleFile(File outputDir, File inputFile, DatabaseCatalogInfo catalog, 663 String extension, int catalogIndex, String createdBy) throws IOException { 664 Map<String, Object> catalogContent = readCatalogContent(catalog.file); 665 if (catalogContent == null) { 666 return null; 667 } 668 669 Map<String, Object> sqlflow = new LinkedHashMap<>(); 670 sqlflow.put("createdBy", createdBy != null ? createdBy : "sqlflow-ingester"); 671 sqlflow.put("format", "sqlflow"); 672 673 Map<String, Object> serverObj = new LinkedHashMap<>(); 674 serverObj.put("name", catalog.serverName); 675 serverObj.put("dbVendor", catalog.dbVendor); 676 677 Map<String, Object> dbObj = new LinkedHashMap<>(); 678 dbObj.put("name", catalog.databaseName); 679 copyCatalogFields(catalogContent, dbObj); 680 681 List<Map<String, Object>> databases = new ArrayList<>(); 682 databases.add(dbObj); 683 serverObj.put("databases", databases); 684 685 List<Map<String, Object>> servers = new ArrayList<>(); 686 servers.add(serverObj); 687 sqlflow.put("servers", servers); 688 sqlflow.put("databases", new ArrayList<>()); 689 690 // Add mergedFrom field to track original shardId 691 List<String> mergedFrom = new ArrayList<>(); 692 mergedFrom.add(catalog.shardId); 693 sqlflow.put("mergedFrom", mergedFrom); 694 695 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_catalog_" + catalogIndex + 696 (extension.isEmpty() ? "" : "." + extension); 697 698 File outputFile = new File(outputDir, fileName); 699 writeJsonFile(outputFile, sqlflow); 700 return outputFile; 701 } 702 703 /** 704 * Read catalog content from file 705 */ 706 private static Map<String, Object> readCatalogContent(File catalogFile) throws IOException { 707 String content = readFileContent(catalogFile); 708 @SuppressWarnings("unchecked") 709 Map<String, Object> catalog = (Map<String, Object>) JSON.parseObject(content); 710 return catalog; 711 } 712 713 /** 714 * Copy catalog fields to database object 715 */ 716 private static void copyCatalogFields(Map<String, Object> catalog, Map<String, Object> dbObj) { 717 if (catalog.containsKey("schemas")) dbObj.put("schemas", catalog.get("schemas")); 718 if (catalog.containsKey("tables")) dbObj.put("tables", catalog.get("tables")); 719 if (catalog.containsKey("views")) dbObj.put("views", catalog.get("views")); 720 if (catalog.containsKey("procedures")) dbObj.put("procedures", catalog.get("procedures")); 721 if (catalog.containsKey("functions")) dbObj.put("functions", catalog.get("functions")); 722 if (catalog.containsKey("triggers")) dbObj.put("triggers", catalog.get("triggers")); 723 if (catalog.containsKey("packages")) dbObj.put("packages", catalog.get("packages")); 724 if (catalog.containsKey("synonyms")) dbObj.put("synonyms", catalog.get("synonyms")); 725 } 726 727 /** 728 * Common method to create source query file 729 */ 730 private static File createSourceQueryFileCommon(File outputDir, File inputFile, String extension, 731 List<Map<String, Object>> queries, List<String> mergedFrom, 732 int startIndex, int endIndex, 733 String createdBy) throws IOException { 734 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_source_" + startIndex + "_" + endIndex + 735 (extension.isEmpty() ? "" : "." + extension); 736 737 File queryFile = new File(outputDir, fileName); 738 739 Map<String, Object> sqlflow = new LinkedHashMap<>(); 740 sqlflow.put("createdBy", createdBy != null ? createdBy : "grabit v1.7.0"); 741 sqlflow.put("databases", new ArrayList<>()); 742 sqlflow.put("mergedFrom", mergedFrom); 743 744 Map<String, Object> serverObj = new LinkedHashMap<>(); 745 serverObj.put("name", "default"); 746 serverObj.put("queries", queries); 747 748 List<Map<String, Object>> servers = new ArrayList<>(); 749 servers.add(serverObj); 750 sqlflow.put("servers", servers); 751 752 writeJsonFile(queryFile, sqlflow); 753 return queryFile; 754 } 755 756 /** 757 * Resolve path relative to base directory 758 */ 759 private static File resolvePath(File baseDir, String path) { 760 if (path == null || path.isEmpty()) { 761 return null; 762 } 763 764 File file = new File(path); 765 if (file.isAbsolute()) { 766 return file; 767 } 768 769 return new File(baseDir, path); 770 } 771 772 /** 773 * Read source records from JSONL file (supports gzip compression) 774 */ 775 private static List<Map<?, ?>> readSourceRecords(File sourceFile, String compression) throws IOException { 776 if (sourceFile == null || !sourceFile.exists()) { 777 return null; 778 } 779 780 List<Map<?, ?>> records = new ArrayList<>(); 781 782 if ("block".equals(compression)) { 783 // Read gzip compressed file 784 try (java.util.zip.GZIPInputStream gzis = new java.util.zip.GZIPInputStream( 785 new java.io.FileInputStream(sourceFile))) { 786 java.io.BufferedReader reader = new java.io.BufferedReader( 787 new java.io.InputStreamReader(gzis, "UTF-8")); 788 String line; 789 while ((line = reader.readLine()) != null) { 790 if (!line.trim().isEmpty()) { 791 Map<?, ?> record = (Map<?, ?>) JSON.parseObject(line); 792 records.add(record); 793 } 794 } 795 } 796 } else { 797 // Read plain JSONL file 798 String content = readFileContent(sourceFile); 799 String[] lines = content.split("\n"); 800 for (String line : lines) { 801 if (!line.trim().isEmpty()) { 802 Map<?, ?> record = (Map<?, ?>) JSON.parseObject(line); 803 records.add(record); 804 } 805 } 806 } 807 808 return records; 809 } 810 811 /** 812 * Helper class for catalog info 813 */ 814 private static class DatabaseCatalogInfo { 815 String serverName; 816 String dbVendor; 817 String databaseName; 818 String shardId; 819 File file; 820 821 DatabaseCatalogInfo(String serverName, String dbVendor, String databaseName, String shardId, File file) { 822 this.serverName = serverName; 823 this.dbVendor = dbVendor; 824 this.databaseName = databaseName; 825 this.shardId = shardId; 826 this.file = file; 827 } 828 } 829 830 /** 831 * Helper class for source info 832 */ 833 private static class DatabaseSourceInfo { 834 String serverName; 835 String dbVendor; 836 String databaseName; 837 String shardId; 838 File file; 839 String compression; 840 841 DatabaseSourceInfo(String serverName, String dbVendor, String databaseName, String shardId, File file, String compression) { 842 this.serverName = serverName; 843 this.dbVendor = dbVendor; 844 this.databaseName = databaseName; 845 this.shardId = shardId; 846 this.file = file; 847 this.compression = compression; 848 } 849 } 850 851 /** 852 * Create query file 853 */ 854 private static File createQueryFile(File outputDir, File inputFile, long startIndex, long endIndex, String extension) { 855 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_" + startIndex + "_" + endIndex + (extension.isEmpty() ? "" : "." + extension); 856 return new File(outputDir, fileName); 857 } 858 859 private static Long getLastLineFromCurrentPos(RandomAccessFile raf, long currentPos, long fileLength, long splitSizeBytes, EDbVendor vendor) throws IOException { 860 raf.seek(currentPos); 861 862 long remainingBytes = fileLength - currentPos; 863 long readSize = Math.min(splitSizeBytes, remainingBytes); 864 865 byte[] buffer = new byte[(int) readSize]; 866 int bytesRead = raf.read(buffer); 867 if (bytesRead <= 0) { 868 return null; 869 } 870 871 String partialContent = new String(buffer, 0, bytesRead, "UTF-8"); 872 if (partialContent.isEmpty()) { 873 return null; 874 } 875 876 TGSqlParser parser = new TGSqlParser(vendor); 877 parser.sqltext = partialContent; 878 parser.getrawsqlstatements(); 879 880 long lastValidatedLineNo = parser.getLastLineNoOfLastStatementBeenValidated(); 881 return lastValidatedLineNo; 882 } 883 884 /** 885 * Find end position of Nth line from specified byte position 886 * Supports multiple newline formats: \n (Unix/Linux), \r\n (Windows), \r (Old Mac) 887 * Uses buffer for batch reading to improve performance 888 */ 889 private static long findLineEndPosition(RandomAccessFile raf, long startPos, long lineCount) throws IOException { 890 raf.seek(startPos); 891 long newlineCount = 0; 892 long fileLength = raf.length(); 893 894 byte[] buffer = new byte[BUFFER_SIZE]; 895 long currentPos = startPos; 896 897 while (currentPos < fileLength) { 898 int toRead = (int) Math.min(buffer.length, fileLength - currentPos); 899 int bytesRead = raf.read(buffer, 0, toRead); 900 if (bytesRead <= 0) { 901 break; 902 } 903 904 for (int i = 0; i < bytesRead; i++) { 905 byte b = buffer[i]; 906 907 if (b == '\n') { 908 newlineCount++; 909 if (newlineCount == lineCount) { 910 return currentPos + i + 1; 911 } 912 } else if (b == '\r') { 913 if (i + 1 < bytesRead) { 914 if (buffer[i + 1] == '\n') { 915 newlineCount++; 916 if (newlineCount == lineCount) { 917 return currentPos + i + 2; 918 } 919 i++; 920 } else { 921 newlineCount++; 922 if (newlineCount == lineCount) { 923 return currentPos + i + 1; 924 } 925 } 926 } else { 927 long savedPos = raf.getFilePointer(); 928 int nextByte = raf.read(); 929 930 if (nextByte == '\n') { 931 newlineCount++; 932 if (newlineCount == lineCount) { 933 return currentPos + i + 2; 934 } 935 currentPos++; 936 } else { 937 newlineCount++; 938 if (newlineCount == lineCount) { 939 return currentPos + i + 1; 940 } 941 if (nextByte != -1) { 942 raf.seek(savedPos); 943 } 944 } 945 } 946 } 947 } 948 949 currentPos += bytesRead; 950 } 951 952 return fileLength; 953 } 954 955 /** 956 * Split file by byte position 957 */ 958 private static File splitFileByPosition(File outputDir, File inputFile, long startPos, long endPos, int fileIndex, long startLineNo, long endLineNo) throws IOException { 959 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_" + fileIndex + "_" + startLineNo + "_" + endLineNo + "." + getFileExtension(inputFile.getName()); 960 File outputFile = new File(outputDir, fileName); 961 962 try (RandomAccessFile rafRead = new RandomAccessFile(inputFile, "r"); 963 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), java.nio.charset.StandardCharsets.UTF_8))) { 964 965 rafRead.seek(startPos); 966 long bytesToRead = endPos - startPos; 967 long bytesRemaining = bytesToRead; 968 969 byte[] buffer = new byte[BUFFER_SIZE]; 970 971 while (bytesRemaining > 0) { 972 int toRead = (int) Math.min(buffer.length, bytesRemaining); 973 int bytesRead = rafRead.read(buffer, 0, toRead); 974 if (bytesRead <= 0) { 975 break; 976 } 977 978 String chunk = new String(buffer, 0, bytesRead, java.nio.charset.StandardCharsets.UTF_8); 979 writer.write(chunk); 980 bytesRemaining -= bytesRead; 981 } 982 983 logger.info("split file " + inputFile.getName() + " (index: " + fileIndex + ", lines: " + startLineNo + "-" + endLineNo + ") from byte " + startPos + " to " + endPos + " to " + outputFile.getName()); 984 985 return outputFile; 986 } 987 } 988}