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}