001package gudusoft.gsqlparser.tools; 002 003/** 004 * Function of this class : 005 * 006 * 1. Take a directory path as input 007 * 2. Read all the sql files in the directory recursively and read the content of the file with .sql extension 008 * 3. Guess the database vendor based on the directory name or file name, if the db vendor is guessed from the directory name, 009 * then all the files in the directory should be of the same db vendor,otherwise guess the db vendor from the filename, otherwise use the default db vendor as mysql 010 * 4. Use TGSqlParser.getrawsqlstatements() to get the raw sql statements from the file, and count the number of sql statements in the file 011 * 5. Collect the following information for each sql file: 012 * - file size in bytes 013 * - number of sql statements 014 * - sql statement type of each sql statement and the count of each type, use TCustomSqlStatement.sqlstatementtype to get the sql statement type 015 * - db vendor 016 * 6. Collect the following information for the entire directory: 017 * - total number of sql files 018 * - total number of sql statements 019 * - total number of bytes 020 * - total number of sql statements by type 021 * 022 * the above information can be grouped by db vendor 023 * 7. Output the information to a csv file 024 * 8. add a new option to save all the sql statements to a file in a single line json format, each sql statement is a json object, 025 * and includes the following elements: 026 * - db vendor 027 * - sql statement type 028 * - sql statement size in bytes 029 * - unique hash id of this sql statement,when calculating the unique hash id, all whitespaces must be ignored 030 * - sql statement 031 * 032 * the file name is sql_statements.json 033 * 034 * Sample output: 035 * ======================= Summary Statistics ======================= 036 * Total SQL Files: 6529 037 * Total Bytes: 25353796 038 * Total SQL Statements: 54617 039 * 040 * --- Average Statistics --- 041 * Average File Size (bytes): 3883.259 042 * Average SQL Statements Per File: 8.365 043 * Average Size Per SQL Statement (bytes): 464.211 044 * 045 * Statistics have been written to c:\prg\tmp\a.csv 046 */ 047 048import gudusoft.gsqlparser.EDbVendor; 049import gudusoft.gsqlparser.TCustomSqlStatement; 050import gudusoft.gsqlparser.TGSqlParser; 051import gudusoft.gsqlparser.TStatementList; 052import java.io.*; 053import java.nio.file.Files; 054import java.util.*; 055import java.text.NumberFormat; 056import java.util.Locale; 057import java.security.MessageDigest; 058import java.security.NoSuchAlgorithmException; 059import java.nio.charset.Charset; 060import java.nio.charset.StandardCharsets; 061 062public class CountSql { 063 064 // Create a formatter for numeric values 065 private final NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.US); 066 067 public static void main(String[] args) { 068 if (args.length < 1) { 069 System.out.println("Usage: java gudusoft.gsqlparser.tools.CountSql <directory_path> [output_csv_path] [show_file_details] [export_json] [json_output_dir]"); 070 System.out.println(" - show_file_details: Optional. Set to 'true' to include individual file details in the output (default: false)"); 071 System.out.println(" - export_json: Optional. Set to 'true' to export all SQL statements as JSON (default: false)"); 072 System.out.println(" - json_output_dir: Optional. Directory to save the JSON output file (default: current directory)"); 073 return; 074 } 075 076 String dirPath = args[0]; 077 String outputPath = args.length > 1 ? args[1] : "sql_statistics.csv"; 078 boolean showFileDetails = args.length > 2 ? Boolean.parseBoolean(args[2]) : false; 079 boolean exportJson = args.length > 3 ? Boolean.parseBoolean(args[3]) : false; 080 String jsonOutputDir = args.length > 4 ? args[4] : "."; 081 082 CountSql counter = new CountSql(); 083 counter.processDirectory(dirPath, outputPath, showFileDetails, exportJson, jsonOutputDir); 084 } 085 086 /** 087 * Process all SQL files in a directory and its subdirectories 088 * @param dirPath Directory path to process 089 * @param outputPath Path to output CSV file 090 * @param showFileDetails Whether to include file details section in the output 091 * @param exportJson Whether to export all SQL statements as JSON 092 * @param jsonOutputDir Directory to save the JSON output file 093 */ 094 public void processDirectory(String dirPath, String outputPath, boolean showFileDetails, boolean exportJson, String jsonOutputDir) { 095 File directory = new File(dirPath); 096 097 if (!directory.exists() || !directory.isDirectory()) { 098 System.err.println("Error: " + dirPath + " is not a valid directory"); 099 return; 100 } 101 102 // Statistics for the entire directory 103 int totalFiles = 0; 104 long totalBytes = 0; 105 int totalStatements = 0; 106 Map<String, Integer> totalStatementsByType = new HashMap<>(); 107 Map<EDbVendor, Map<String, Integer>> statementsByVendor = new HashMap<>(); 108 Map<EDbVendor, Integer> filesByVendor = new HashMap<>(); 109 Map<EDbVendor, Long> bytesByVendor = new HashMap<>(); 110 Map<EDbVendor, Integer> statementsByVendorCount = new HashMap<>(); 111 112 // List to store all SQL statement details if JSON export is requested 113 List<SqlStatementDetail> allStatements = exportJson ? new ArrayList<>() : null; 114 115 // CSV output preparation 116 try (PrintWriter csvWriter = newUtf8Writer(new File(outputPath))) { 117 // Write headers for file details section only if requested 118 if (showFileDetails) { 119 csvWriter.println("File Path,File Size (bytes),DB Vendor,SQL Statements Count,Statement Types"); 120 } 121 122 // Process files 123 List<File> sqlFiles = findSqlFiles(directory); 124 totalFiles = sqlFiles.size(); 125 126 for (File sqlFile : sqlFiles) { 127 FileStatistics stats = processFile(sqlFile, dirPath, exportJson); 128 totalBytes += stats.size; 129 totalStatements += stats.statementCount; 130 131 // Update vendor-specific statistics 132 EDbVendor vendor = stats.vendor; 133 filesByVendor.put(vendor, filesByVendor.getOrDefault(vendor, 0) + 1); 134 bytesByVendor.put(vendor, bytesByVendor.getOrDefault(vendor, 0L) + stats.size); 135 statementsByVendorCount.put(vendor, statementsByVendorCount.getOrDefault(vendor, 0) + stats.statementCount); 136 137 // Update statement types 138 for (Map.Entry<String, Integer> entry : stats.statementTypeCount.entrySet()) { 139 String type = entry.getKey(); 140 int count = entry.getValue(); 141 142 // Update overall counts 143 totalStatementsByType.put(type, totalStatementsByType.getOrDefault(type, 0) + count); 144 145 // Update vendor-specific counts 146 Map<String, Integer> vendorTypes = statementsByVendor.getOrDefault(vendor, new HashMap<>()); 147 vendorTypes.put(type, vendorTypes.getOrDefault(type, 0) + count); 148 statementsByVendor.put(vendor, vendorTypes); 149 } 150 151 // Collect SQL statements for JSON export if requested 152 if (exportJson && stats.statementDetails != null) { 153 allStatements.addAll(stats.statementDetails); 154 } 155 156 // Write file details to CSV with formatted numbers only if requested 157 if (showFileDetails) { 158 csvWriter.println(sqlFile.getPath() + "," + 159 formatNumber(stats.size) + "," + 160 vendor + "," + 161 formatNumber(stats.statementCount) + "," + 162 formatStatementTypes(stats.statementTypeCount)); 163 } 164 } 165 166 // Write summary section with formatted numbers 167 // If file details were included, add a blank line to separate sections 168 if (showFileDetails) { 169 csvWriter.println(); 170 } 171 172 csvWriter.println("Summary Statistics"); 173 csvWriter.println("Total SQL Files," + formatNumber(totalFiles)); 174 csvWriter.println("Total Bytes," + formatNumber(totalBytes)); 175 csvWriter.println("Total SQL Statements," + formatNumber(totalStatements)); 176 177 // Calculate and add average statistics 178 if (totalFiles > 0) { 179 double avgFileSize = (double) totalBytes / totalFiles; 180 double avgStatementsPerFile = (double) totalStatements / totalFiles; 181 182 csvWriter.println("Average File Size (bytes)," + formatNumber(avgFileSize)); 183 csvWriter.println("Average SQL Statements Per File," + formatNumber(avgStatementsPerFile)); 184 185 // Add average size per SQL statement 186 if (totalStatements > 0) { 187 double avgSizePerStatement = (double) totalBytes / totalStatements; 188 csvWriter.println("Average Size Per SQL Statement (bytes)," + formatNumber(avgSizePerStatement)); 189 } 190 } 191 192 // Write statement types summary - sorted by count (descending) 193 csvWriter.println("\nStatement Types Summary"); 194 List<Map.Entry<String, Integer>> sortedStatementTypes = new ArrayList<>(totalStatementsByType.entrySet()); 195 sortedStatementTypes.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); 196 197 for (Map.Entry<String, Integer> entry : sortedStatementTypes) { 198 csvWriter.println(entry.getKey() + "," + formatNumber(entry.getValue())); 199 } 200 201 // Write vendor-specific summary - sorted by statement count (descending) 202 csvWriter.println("\nStatistics by DB Vendor"); 203 csvWriter.println("DB Vendor,Files,Bytes,Statements"); 204 205 List<EDbVendor> sortedVendors = new ArrayList<>(statementsByVendorCount.keySet()); 206 sortedVendors.sort((v1, v2) -> statementsByVendorCount.get(v2).compareTo(statementsByVendorCount.get(v1))); 207 208 for (EDbVendor vendor : sortedVendors) { 209 csvWriter.println(vendor + "," + 210 formatNumber(filesByVendor.get(vendor)) + "," + 211 formatNumber(bytesByVendor.get(vendor)) + "," + 212 formatNumber(statementsByVendorCount.get(vendor))); 213 } 214 215 // Write statement types by vendor - vendors and types both sorted by count (descending) 216 csvWriter.println("\nStatement Types by DB Vendor"); 217 csvWriter.println("DB Vendor,Statement Type,Count"); 218 219 for (EDbVendor vendor : sortedVendors) { 220 Map<String, Integer> typeCount = statementsByVendor.get(vendor); 221 222 // Skip if typeCount is null or empty 223 if (typeCount != null && !typeCount.isEmpty()) { 224 // Sort statement types by count for this vendor 225 List<Map.Entry<String, Integer>> sortedTypes = new ArrayList<>(typeCount.entrySet()); 226 sortedTypes.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); 227 228 for (Map.Entry<String, Integer> typeEntry : sortedTypes) { 229 csvWriter.println(vendor + "," + 230 typeEntry.getKey() + "," + 231 formatNumber(typeEntry.getValue())); 232 } 233 } else { 234 // If no statement types found for this vendor, output a message 235 csvWriter.println(vendor + ",No specific statement types found,0"); 236 } 237 } 238 239 System.out.println("Statistics have been written to " + outputPath); 240 241 } catch (IOException e) { 242 System.err.println("Error writing to CSV file: " + e.getMessage()); 243 } 244 245 // Export SQL statements to JSON if requested 246 String jsonFilePath = null; 247 if (exportJson) { 248 if (allStatements != null && !allStatements.isEmpty()) { 249 jsonFilePath = exportStatementsToJson(allStatements, jsonOutputDir); 250 if (jsonFilePath != null) { 251 System.out.println("SQL statements have been exported to: " + jsonFilePath); 252 } 253 } else { 254 System.out.println("No JSON file created: No SQL statements were collected"); 255 } 256 } else { 257 System.out.println("No JSON file created: JSON export not requested"); 258 } 259 260 // Print summary statistics to the console - simplified version 261 System.out.println("\n======================= Summary Statistics ======================="); 262 System.out.println("Total SQL Files: " + formatNumber(totalFiles)); 263 System.out.println("Total Bytes: " + formatNumber(totalBytes)); 264 System.out.println("Total SQL Statements: " + formatNumber(totalStatements)); 265 266 // Calculate and add average statistics 267 if (totalFiles > 0) { 268 double avgFileSize = (double) totalBytes / totalFiles; 269 double avgStatementsPerFile = (double) totalStatements / totalFiles; 270 271 System.out.println("\n--- Average Statistics ---"); 272 System.out.println("Average File Size (bytes): " + formatNumber(avgFileSize)); 273 System.out.println("Average SQL Statements Per File: " + formatNumber(avgStatementsPerFile)); 274 275 // Add average size per SQL statement 276 if (totalStatements > 0) { 277 double avgSizePerStatement = (double) totalBytes / totalStatements; 278 System.out.println("Average Size Per SQL Statement (bytes): " + formatNumber(avgSizePerStatement)); 279 } 280 } 281 282 // Print file paths summary 283 System.out.println("\n--- Output Files ---"); 284 System.out.println("CSV Statistics: " + new File(outputPath).getAbsolutePath()); 285 if (jsonFilePath != null) { 286 System.out.println("JSON Statements: " + jsonFilePath); 287 } 288 289 System.out.println("\nStatistics have been written to " + outputPath); 290 } 291 292 /** 293 * Export SQL statements to JSON file 294 * @param statements List of SQL statement details 295 * @param outputDir Directory to save the JSON file 296 * @return The full absolute path of the created JSON file, or null if creation failed 297 */ 298 private String exportStatementsToJson(List<SqlStatementDetail> statements, String outputDir) { 299 // Create File object for the output directory 300 File outputDirectory = new File(outputDir); 301 302 // Create the directory if it doesn't exist 303 if (!outputDirectory.exists()) { 304 if (!outputDirectory.mkdirs()) { 305 System.err.println("No JSON file created: Could not create directory: " + outputDir); 306 return null; 307 } 308 } 309 310 // Ensure the path is a directory 311 if (!outputDirectory.isDirectory()) { 312 System.err.println("No JSON file created: Specified JSON output path is not a directory: " + outputDir); 313 return null; 314 } 315 316 // Create the full path for the JSON file 317 String jsonFileName = "sql_statements.json"; 318 File jsonFile = new File(outputDirectory, jsonFileName); 319 String jsonFilePath = jsonFile.getAbsolutePath(); // Use absolute path for clearer output 320 321 try (PrintWriter jsonWriter = newUtf8Writer(jsonFile)) { 322 for (SqlStatementDetail stmt : statements) { 323 // Write each statement as a single line JSON object 324 jsonWriter.println(stmt.toJson()); 325 } 326 327 return jsonFilePath; 328 329 } catch (IOException e) { 330 System.err.println("No JSON file created: Error writing to JSON file: " + e.getMessage()); 331 return null; 332 } 333 } 334 335 /** 336 * Format number without thousands separators 337 * @param number The number to format 338 * @return Formatted string without thousands separators 339 */ 340 private String formatNumber(Number number) { 341 NumberFormat formatter = NumberFormat.getNumberInstance(Locale.US); 342 formatter.setGroupingUsed(false); // 禁用分组(不使用千分号) 343 344 // 对于整数值,不显示小数部分 345 if (number instanceof Integer || number instanceof Long) { 346 formatter.setMaximumFractionDigits(0); 347 } 348 349 return formatter.format(number); 350 } 351 352 /** 353 * Format the statement types map as a string for CSV output 354 */ 355 private String formatStatementTypes(Map<String, Integer> statementTypes) { 356 StringBuilder sb = new StringBuilder(); 357 for (Map.Entry<String, Integer> entry : statementTypes.entrySet()) { 358 if (sb.length() > 0) { 359 sb.append(";"); 360 } 361 sb.append(entry.getKey()).append(":").append(formatNumber(entry.getValue())); 362 } 363 return sb.toString(); 364 } 365 366 /** 367 * Find all SQL files in a directory and its subdirectories 368 * @param directory Directory to search 369 * @return List of SQL files 370 */ 371 private List<File> findSqlFiles(File directory) { 372 List<File> sqlFiles = new ArrayList<>(); 373 findSqlFilesRecursive(directory, sqlFiles); 374 return sqlFiles; 375 } 376 377 /** 378 * Recursively find SQL files 379 */ 380 private void findSqlFilesRecursive(File directory, List<File> sqlFiles) { 381 File[] files = directory.listFiles(); 382 if (files != null) { 383 for (File file : files) { 384 if (file.isDirectory()) { 385 findSqlFilesRecursive(file, sqlFiles); 386 } else if (file.getName().toLowerCase(Locale.ROOT).endsWith(".sql")) { 387 sqlFiles.add(file); 388 } 389 } 390 } 391 } 392 393 /** 394 * Process a single SQL file 395 * @param file SQL file to process 396 * @param rootDirPath Root directory path for DB vendor detection 397 * @param collectStatements Whether to collect individual SQL statements 398 * @return Statistics for the file 399 */ 400 private FileStatistics processFile(File file, String rootDirPath, boolean collectStatements) { 401 FileStatistics stats = new FileStatistics(); 402 stats.size = file.length(); 403 404 // Skip processing if file size is zero 405 if (stats.size == 0) { 406 System.out.println("Skipping empty file: " + file.getPath()); 407 return stats; 408 } 409 410 // Determine DB vendor 411 stats.vendor = guessDbVendor(file, rootDirPath); 412 413 try { 414 // Read file content 415 String sqlContent = readSqlFile(file); 416 417 // Skip files that are empty or only contain whitespace 418 if (sqlContent.trim().isEmpty()) { 419 System.out.println("Skipping file with only whitespace: " + file.getPath()); 420 return stats; 421 } 422 423 // Parse SQL statements 424 TGSqlParser parser = new TGSqlParser(stats.vendor); 425 parser.sqltext = sqlContent; 426 427 // Get raw SQL statements 428 int iRet = parser.getrawsqlstatements(); 429 if (iRet != 0) { 430 System.err.println("Error parsing SQL statements: " + iRet+" for file "+file.getPath()+" with vendor "+stats.vendor); 431 return stats; 432 } 433 TStatementList statements = parser.sqlstatements; 434 stats.statementCount = statements.size(); 435 436 // Initialize statement details list if collecting statements 437 if (collectStatements) { 438 stats.statementDetails = new ArrayList<>(); 439 } 440 441 // Count statement types and collect statements if requested 442 for (int i = 0; i < statements.size(); i++) { 443 TCustomSqlStatement stmt = statements.get(i); 444 String stmtType = stmt.sqlstatementtype.toString(); 445 stats.statementTypeCount.put(stmtType, stats.statementTypeCount.getOrDefault(stmtType, 0) + 1); 446 447 // Collect statement details if requested 448 if (collectStatements) { 449 SqlStatementDetail detail = new SqlStatementDetail(); 450 detail.dbVendor = stats.vendor.toString(); 451 detail.statementType = stmtType; 452 453 // Get the SQL statement text 454 String sqlText = stmt.toString(); 455 detail.sqlStatement = sqlText; 456 457 // Calculate SQL statement size (bytes in UTF-8 encoding) 458 detail.sqlSize = sqlText.getBytes(StandardCharsets.UTF_8).length; 459 460 // Calculate and set the unique hash ID (ignoring whitespaces) 461 detail.uniqueHashId = calculateHashId(sqlText); 462 463 stats.statementDetails.add(detail); 464 } 465 } 466 467 } catch (IOException e) { 468 System.err.println("Error reading file " + file.getPath() + ": " + e.getMessage()); 469 } 470 471 return stats; 472 } 473 474 /** 475 * Guess the database vendor based on directory name or file name 476 * @param file SQL file 477 * @param rootDirPath Root directory path 478 * @return Database vendor 479 */ 480 /** 481 * Reads a SQL file. A byte order mark proves the encoding: it picks UTF-8, UTF-16 or UTF-32 and is 482 * dropped, by the same rules as {@code SQLUtil.getFileContent} (Mantis 4801: reading every file in 483 * the platform charset split UTF-16 files into garbage, and a UTF-8 file with a mark gave no 484 * statements). A file without one is read in the platform charset, as before, which 485 * {@code -Dfile.encoding} sets: its bytes can be valid in two charsets at once, such as C2 A3, which 486 * is UTF-8 for a pound sign and GBK for another character, so no guess here could avoid turning a 487 * file that was read correctly into one that is not. getFileContent itself would read such a file 488 * as UTF-8 always, and it also rewrites U+00A0 to a space and hides read errors. 489 */ 490 static String readSqlFile(File file) throws IOException { 491 byte[] bytes = Files.readAllBytes(file.toPath()); 492 if (startsWith(bytes, 0x00, 0x00, 0xFE, 0xFF)) { 493 return new String(bytes, 4, bytes.length - 4, "UTF-32BE"); 494 } 495 if (startsWith(bytes, 0xFF, 0xFE, 0x00, 0x00)) { 496 return new String(bytes, 4, bytes.length - 4, "UTF-32LE"); 497 } 498 if (startsWith(bytes, 0xEF, 0xBB, 0xBF)) { 499 return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); 500 } 501 if (startsWith(bytes, 0xFE, 0xFF)) { 502 return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16BE); 503 } 504 if (startsWith(bytes, 0xFF, 0xFE)) { 505 return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16LE); 506 } 507 return new String(bytes, Charset.defaultCharset()); 508 } 509 510 private static boolean startsWith(byte[] bytes, int... prefix) { 511 if (bytes.length < prefix.length) { 512 return false; 513 } 514 for (int i = 0; i < prefix.length; i++) { 515 if ((bytes[i] & 0xFF) != prefix[i]) { 516 return false; 517 } 518 } 519 return true; 520 } 521 522 /** 523 * A writer that writes UTF-8 whatever the platform charset: JSON must be UTF-8, and FileWriter wrote 524 * a windows-1252 e-acute as the single byte E9 (Mantis 4801). Like FileWriter, it replaces what it 525 * cannot encode instead of failing, which PrintWriter would otherwise swallow. 526 */ 527 private static PrintWriter newUtf8Writer(File file) throws IOException { 528 return new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); 529 } 530 531 private EDbVendor guessDbVendor(File file, String rootDirPath) { 532 // Check directory path recursively 533 File currentDir = file.getParentFile(); 534 File rootDir = new File(rootDirPath); 535 536 // Convert both paths to absolute to ensure proper comparison 537 String absoluteRootPath = rootDir.getAbsolutePath(); 538 539 // Traverse up the directory tree until root directory 540 while (currentDir != null && 541 currentDir.getAbsolutePath().startsWith(absoluteRootPath)) { 542 543 String dirName = currentDir.getName().toLowerCase(Locale.ROOT); 544 EDbVendor vendorFromDir = vendorFromName(dirName); 545 if (vendorFromDir != null) { 546 return vendorFromDir; 547 } 548 549 // Move up to parent directory 550 currentDir = currentDir.getParentFile(); 551 } 552 553 // If no vendor found in directory structure, check file name 554 String fileName = file.getName().toLowerCase(Locale.ROOT); 555 EDbVendor vendorFromFile = vendorFromName(fileName); 556 if (vendorFromFile != null) { 557 return vendorFromFile; 558 } 559 560 // Default to generic 561 return EDbVendor.dbvgeneric; 562 } 563 564 /** 565 * Determine vendor from a name (directory or file) 566 * Supports a comprehensive list of database vendors 567 * @param name Directory or file name to analyze 568 * @return The identified database vendor or null if not recognized 569 */ 570 private EDbVendor vendorFromName(String name) { 571 name = name.toLowerCase(Locale.ROOT); 572 573 // Oracle 574 if (name.contains("oracle") || name.contains("ora") || name.contains("plsql")) { 575 return EDbVendor.dbvoracle; 576 } 577 // MySQL 578 else if (name.contains("mysql") || name.contains("maria")) { 579 return EDbVendor.dbvmysql; 580 } 581 // PostgreSQL 582 else if (name.contains("postgresql") || name.contains("postgres") || name.contains("pgsql")) { 583 return EDbVendor.dbvpostgresql; 584 } 585 // SQL Server 586 else if (name.contains("sqlserver") || name.contains("mssql") || name.contains("sql_server") || name.contains("tsql")) { 587 return EDbVendor.dbvmssql; 588 } 589 // DB2 590 else if (name.contains("db2")) { 591 return EDbVendor.dbvdb2; 592 } 593 // Sybase 594 else if (name.contains("sybase") || name.contains("ase")) { 595 return EDbVendor.dbvsybase; 596 } 597 // Informix 598 else if (name.contains("informix")) { 599 return EDbVendor.dbvinformix; 600 } 601 // Teradata 602 else if (name.contains("teradata")) { 603 return EDbVendor.dbvteradata; 604 } 605 // Netezza 606 else if (name.contains("netezza")) { 607 return EDbVendor.dbvnetezza; 608 } 609 // Hive 610 else if (name.contains("hive")) { 611 return EDbVendor.dbvhive; 612 } 613 // Greenplum 614 else if (name.contains("greenplum")) { 615 return EDbVendor.dbvgreenplum; 616 } 617 // Redshift 618 else if (name.contains("redshift")) { 619 return EDbVendor.dbvredshift; 620 } 621 // Snowflake 622 else if (name.contains("snowflake")) { 623 return EDbVendor.dbvsnowflake; 624 } 625 // Vertica 626 else if (name.contains("vertica")) { 627 return EDbVendor.dbvvertica; 628 } 629 // Athena 630 else if (name.contains("athena")) { 631 return EDbVendor.dbvathena; 632 } 633 // BigQuery 634 else if (name.contains("bigquery") || name.contains("big_query") || name.contains("big-query")) { 635 return EDbVendor.dbvbigquery; 636 } 637 // Couchbase 638 else if (name.contains("couchbase") || name.contains("n1ql")) { 639 return EDbVendor.dbvcouchbase; 640 } 641 // Databricks 642 else if (name.contains("databricks") || name.contains("delta")) { 643 return EDbVendor.dbvdatabricks; 644 } 645 // DAX 646 else if (name.contains("dax") || name.contains("powerbi")) { 647 return EDbVendor.dbvdax; 648 } 649 // GaussDB 650 else if (name.contains("gaussdb") || name.contains("gauss")) { 651 return EDbVendor.dbvgaussdb; 652 } 653 // EDB Postgres Advanced Server 654 else if (name.contains("edb") || name.contains("epas") || name.contains("enterprisedb")) { 655 return EDbVendor.dbvedb; 656 } 657 // HANA 658 else if (name.contains("hana") || name.contains("sap")) { 659 return EDbVendor.dbvhana; 660 } 661 // Impala 662 else if (name.contains("impala")) { 663 return EDbVendor.dbvimpala; 664 } 665 // MDX 666 else if (name.contains("mdx") || name.contains("multidimensional")) { 667 return EDbVendor.dbvmdx; 668 } 669 // ODBC 670 else if (name.contains("odbc")) { 671 return EDbVendor.dbvodbc; 672 } 673 // OpenEdge 674 else if (name.contains("openedge") || name.contains("progress")) { 675 return EDbVendor.dbvopenedge; 676 } 677 // Presto 678 else if (name.contains("presto")) { 679 return EDbVendor.dbvpresto; 680 } 681 // SparkSQL 682 else if (name.contains("sparksql") || name.contains("spark-sql") || name.contains("spark_sql") || name.contains("spark")) { 683 return EDbVendor.dbvsparksql; 684 } 685 // Trino 686 else if (name.contains("trino") || name.contains("starburst")) { 687 return EDbVendor.dbvtrino; 688 } 689 690 return null; 691 } 692 693 /** 694 * Calculate unique hash ID for SQL statement ignoring all whitespaces 695 * This implementation uses SHA-256 and will produce consistent results across languages 696 * when implementing the same algorithm. 697 * @param sql SQL statement to hash 698 * @return Unique hash ID as a lowercase hex string 699 */ 700 public static String calculateHashId(String sql) { 701 if (sql == null || sql.isEmpty()) { 702 return ""; 703 } 704 705 try { 706 // 1. Normalize the SQL by removing ALL whitespace 707 String noWhitespace = sql.replaceAll("\\s+", ""); 708 709 // 2. Convert to bytes using UTF-8 encoding (consistent across platforms) 710 byte[] sqlBytes = noWhitespace.getBytes(StandardCharsets.UTF_8); 711 712 // 3. Calculate SHA-256 hash (widely available in all languages) 713 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 714 byte[] hashBytes = digest.digest(sqlBytes); 715 716 // 4. Convert to lowercase hexadecimal (standard format across languages) 717 StringBuilder hexString = new StringBuilder(); 718 for (byte b : hashBytes) { 719 String hex = Integer.toHexString(0xff & b); 720 if (hex.length() == 1) { 721 hexString.append('0'); // Ensure two characters for each byte 722 } 723 hexString.append(hex); 724 } 725 726 return hexString.toString(); 727 } catch (NoSuchAlgorithmException e) { 728 System.err.println("Error calculating hash ID: " + e.getMessage()); 729 return "hash_error_" + System.currentTimeMillis(); 730 } 731 } 732 733 /** 734 * Class to hold statistics for a single SQL file 735 */ 736 private static class FileStatistics { 737 long size; 738 EDbVendor vendor; 739 int statementCount; 740 Map<String, Integer> statementTypeCount = new HashMap<>(); 741 List<SqlStatementDetail> statementDetails; // For JSON export 742 } 743 744 /** 745 * Class to hold details of a single SQL statement for JSON export 746 */ 747 private static class SqlStatementDetail { 748 String dbVendor; 749 String statementType; 750 int sqlSize; // New field for SQL statement size 751 String sqlStatement; 752 String uniqueHashId; // Field for unique hash ID 753 754 /** 755 * Convert to JSON format 756 * @return JSON representation of this SQL statement 757 */ 758 public String toJson() { 759 // Simple JSON string building (no external library) 760 StringBuilder json = new StringBuilder("{"); 761 json.append("\"dbVendor\":\"").append(escapeJson(dbVendor)).append("\","); 762 json.append("\"statementType\":\"").append(escapeJson(statementType)).append("\","); 763 json.append("\"sqlSize\":").append(sqlSize).append(","); 764 json.append("\"uniqueHashId\":\"").append(escapeJson(uniqueHashId)).append("\","); 765 json.append("\"sqlStatement\":\"").append(escapeJson(sqlStatement)).append("\""); 766 json.append("}"); 767 return json.toString(); 768 } 769 770 /** 771 * Escape special characters for JSON string 772 * @param str String to escape 773 * @return Escaped string 774 */ 775 private String escapeJson(String str) { 776 if (str == null) { 777 return ""; 778 } 779 780 StringBuilder sb = new StringBuilder(); 781 for (int i = 0; i < str.length(); i++) { 782 char ch = str.charAt(i); 783 switch (ch) { 784 case '\"': 785 sb.append("\\\""); 786 break; 787 case '\\': 788 sb.append("\\\\"); 789 break; 790 case '\b': 791 sb.append("\\b"); 792 break; 793 case '\f': 794 sb.append("\\f"); 795 break; 796 case '\n': 797 sb.append("\\n"); 798 break; 799 case '\r': 800 sb.append("\\r"); 801 break; 802 case '\t': 803 sb.append("\\t"); 804 break; 805 default: 806 if (ch < 0x20) { 807 // JSON allows no raw control character inside a string (RFC 8259, 808 // section 7); one in the SQL text made the whole line invalid (Mantis 4800) 809 sb.append(String.format("\\u%04x", (int) ch)); 810 } else { 811 sb.append(ch); 812 } 813 } 814 } 815 return sb.toString(); 816 } 817 } 818}