diff --git a/cli/src/commands/wheels/base.cfc b/cli/src/commands/wheels/base.cfc index c1b438a56..82d08af1c 100644 --- a/cli/src/commands/wheels/base.cfc +++ b/cli/src/commands/wheels/base.cfc @@ -1322,83 +1322,100 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { return local.databases; } - /** - * Get database connection - * This function should be added to base.cfc - */ - private struct function getDatabaseConnection(required struct dsInfo, required string dbType, string systemDatabase = "") { - local.result = { - success: false, - connection: "", - error: "", - driverClass: "" +private struct function getDatabaseConnection( + required struct dsInfo, + required string dbType +) { + local.conn = ""; + + // Get database configuration + local.config = getDatabaseConfig( arguments.dbType, arguments.dsInfo ); + + // Try each driver class + local.driverLoaded = false; + + for ( local.driverClass in local.config.driverClasses ) { + + // IMPORTANT: + // CommandBox CLI + Lucee cannot reliably use Class.forName() + // for SQLite, even when the driver is present on the JVM. + // SQLite JDBC (JDBC 4+) auto-registers, so touching the class + // is sufficient. + if ( arguments.dbType == "SQLite" || arguments.dbType == "SQLite3" ) { + CreateObject( "java", local.driverClass ); + local.driverLoaded = true; + break; + } + + // All other databases keep existing behavior + CreateObject( "java", "java.lang.Class" ).forName( local.driverClass ); + local.driverLoaded = true; + break; + } + + if ( !local.driverLoaded ) { + return { + success : false, + error : "No suitable JDBC driver found for #arguments.dbType#. Tried: " + & ArrayToList( local.config.driverClasses, ", " ), + connection: "" }; - - try { - // Get database-specific configuration - local.dbConfig = getDatabaseConfig(arguments.dbType, arguments.dsInfo, arguments.systemDatabase); - - // Build connection URL - local.url = buildJDBCUrl(local.dbConfig.tempDS); - local.username = local.dbConfig.tempDS.username ?: ""; - local.password = local.dbConfig.tempDS.password ?: ""; - - detailOutput.output("Connecting to " & arguments.dbType & " database..."); - - // Try to load driver - local.driver = ""; - local.driverFound = false; - - for (local.driverClass in local.dbConfig.driverClasses) { - try { - local.driver = createObject("java", local.driverClass); - local.result.driverClass = local.driverClass; - local.driverFound = true; - detailOutput.statusSuccess("Driver found: " & local.driverClass); - break; - } catch (any driverError) { - // Continue trying other drivers + } + + // Create properties + local.props = CreateObject( "java", "java.util.Properties" ).init(); + + // Common credentials + if ( Len( arguments.dsInfo.username ) ) { + local.props.setProperty( "user", arguments.dsInfo.username ); + } + if ( Len( arguments.dsInfo.password ) ) { + local.props.setProperty( "password", arguments.dsInfo.password ); + } + + // Database-specific properties + switch ( arguments.dbType ) { + + case "SQLite": + case "SQLite3": + + local.props.setProperty( "busy_timeout", "5000" ); + local.props.setProperty( "journal_mode", "WAL" ); + local.props.setProperty( "synchronous", "NORMAL" ); + + // Ensure database directory exists + if ( + Len( local.config.tempDS.database ) + && !FileExists( local.config.tempDS.database ) + ) { + local.dbDir = GetDirectoryFromPath( + local.config.tempDS.database + ); + + if ( Len( local.dbDir ) && !DirectoryExists( local.dbDir ) ) { + DirectoryCreate( local.dbDir, true ); } } - - if (!local.driverFound) { - local.result.error = "No " & arguments.dbType & " driver found. Ensure JDBC driver is in classpath."; - return local.result; - } - - // Create properties for connection - local.props = createObject("java", "java.util.Properties"); - local.props.setProperty("user", local.username); - local.props.setProperty("password", local.password); - - // Test if driver accepts the URL - if (!local.driver.acceptsURL(local.url)) { - local.result.error = arguments.dbType & " driver does not accept the URL format"; - return local.result; - } - - // Connect using driver directly - local.conn = local.driver.connect(local.url, local.props); - - if (isNull(local.conn)) { - local.result.error = "Failed to establish connection to " & arguments.dbType; - return local.result; - } - - local.result.success = true; - local.result.connection = local.conn; - detailOutput.statusSuccess("Connected successfully to " & arguments.dbType & " database!"); - return local.result; - - } catch (any e) { - local.result.error = e.message; - if (StructKeyExists(e, "detail")) { - local.result.error &= " - " & e.detail; - } - return local.result; - } + break; } + // Get connection (DriverManager will auto-discover SQLite driver) + local.driverManager = CreateObject( "java", "java.sql.DriverManager" ); + local.conn = local.driverManager.getConnection( + local.config.jdbcUrl, + local.props + ); + + // Test connection + local.conn.setAutoCommit( false ); + + return { + success : true, + connection : local.conn, + jdbcUrl : local.config.jdbcUrl + }; +} + /** * Get database-specific configuration * This function should also be in base.cfc @@ -1406,7 +1423,8 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { private struct function getDatabaseConfig(required string dbType, required struct dsInfo, string systemDatabase = "") { local.config = { tempDS: Duplicate(arguments.dsInfo), - driverClasses: [] + driverClasses: [], + jdbcUrl: "" }; switch (arguments.dbType) { @@ -1421,6 +1439,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { "com.mysql.jdbc.Driver", "org.mariadb.jdbc.Driver" ]; + local.config.jdbcUrl = "jdbc:mysql://#local.config.tempDS.host#:#local.config.tempDS.port#/#local.config.tempDS.database#"; break; case "PostgreSQL": @@ -1433,6 +1452,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { "org.postgresql.Driver", "postgresql.Driver" ]; + local.config.jdbcUrl = "jdbc:postgresql://#local.config.tempDS.host#:#local.config.tempDS.port#/#local.config.tempDS.database#"; break; case "SQLServer": @@ -1445,6 +1465,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { local.config.driverClasses = [ "com.microsoft.sqlserver.jdbc.SQLServerDriver" ]; + local.config.jdbcUrl = "jdbc:sqlserver://#local.config.tempDS.host#:#local.config.tempDS.port#;databaseName=#local.config.tempDS.database#"; break; case "Oracle": @@ -1458,6 +1479,12 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { "oracle.jdbc.OracleDriver", "oracle.jdbc.driver.OracleDriver" ]; + // Build Oracle JDBC URL + if (StructKeyExists(arguments.dsInfo, "serviceName") && Len(arguments.dsInfo.serviceName)) { + local.config.jdbcUrl = "jdbc:oracle:thin:@//#local.config.tempDS.host#:#local.config.tempDS.port#/#arguments.dsInfo.serviceName#"; + } else { + local.config.jdbcUrl = "jdbc:oracle:thin:@#local.config.tempDS.host#:#local.config.tempDS.port#:#local.config.tempDS.database#"; + } break; case "H2": @@ -1465,6 +1492,48 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { local.config.driverClasses = [ "org.h2.Driver" ]; + local.config.jdbcUrl = "jdbc:h2:#local.config.tempDS.database#"; + break; + + case "SQLite": + case "SQLite3": + // SQLite uses file path as database + // For SQLite, the "database" field is actually a file path + if (Len(arguments.dsInfo.database)) { + local.dbPath = arguments.dsInfo.database; + + // Resolve relative paths + if (!FileExists(local.dbPath)) { + // Try relative to current directory + local.appPath = getCWD(); + local.resolvedPath = fileSystemUtil.resolvePath(local.appPath & "/" & local.dbPath); + if (FileExists(local.resolvedPath)) { + local.dbPath = local.resolvedPath; + } + } + + local.config.tempDS.database = local.dbPath; + } + + local.config.driverClasses = [ + "org.sqlite.JDBC", + "org.xerial.sqlite.JDBC", + "SQLite.JDBCDriver" + ]; + + // Build SQLite JDBC URL + if (Len(local.config.tempDS.database)) { + local.config.jdbcUrl = "jdbc:sqlite:" & local.config.tempDS.database; + } else { + local.config.jdbcUrl = "jdbc:sqlite:"; + } + + // SQLite-specific connection properties + local.config.connectionProperties = { + "busy_timeout": "5000", + "journal_mode": "WAL", + "synchronous": "NORMAL" + }; break; } diff --git a/cli/src/commands/wheels/db/dump.cfc b/cli/src/commands/wheels/db/dump.cfc index 7c4c067ad..26a6e340f 100644 --- a/cli/src/commands/wheels/db/dump.cfc +++ b/cli/src/commands/wheels/db/dump.cfc @@ -134,8 +134,8 @@ component extends="../base" { } else if (local.dsInfo.driver == "MSSQL" || local.dsInfo.driver == "MSSQLServer") { local.ext = "bak"; } - - arguments.output = fileSystemUtil.resolvePath("dump_" & local.selectedDatabase & "_" & local.timestamp & "." & local.ext); + + arguments.output = local.selectedDatabase & "_" & local.timestamp & "." & local.ext; if (arguments.compress) { arguments.output &= ".gz"; } @@ -202,6 +202,10 @@ component extends="../base" { case "Oracle": local.success = dumpOracle(local.dsInfo, arguments); break; + case "SQLite": + case "SQLite3": + local.success = dumpSQLite(local.dsInfo, arguments); + break; default: detailOutput.error("Database dump not supported for driver: " & local.dbType); detailOutput.statusInfo("Please use your database management tools to export the database."); @@ -1239,6 +1243,574 @@ component extends="../base" { } } + private boolean function dumpSQLite(required struct dsInfo, required struct options) { + try { + detailOutput.output("Preparing SQLite database export..."); + + /* ---------------------------------------------------- + 1. Get database file path + ---------------------------------------------------- */ + local.databasePath = arguments.dsInfo.database; + + // For SQLite, database is usually a file path + if (!Len(local.databasePath)) { + detailOutput.error("SQLite database path not specified"); + return false; + } + // Resolve the database path + local.resolvedPath = fileSystemUtil.resolvePath(local.databasePath); + + // Check if database file exists + if (!FileExists(local.resolvedPath)) { + // Try relative to app directory + local.appPath = getCWD(); + local.altPath = fileSystemUtil.resolvePath(local.appPath & "/" & local.databasePath); + + if (FileExists(local.altPath)) { + local.resolvedPath = local.altPath; + } else { + detailOutput.error("SQLite database file not found: " & local.databasePath); + detailOutput.output("Tried locations:"); + detailOutput.output("1. " & local.resolvedPath, true); + detailOutput.output("2. " & local.altPath, true); + return false; + } + } + + detailOutput.statusInfo("Database file: " & local.resolvedPath); + + /* ---------------------------------------------------- + 2. Generate output file path + ---------------------------------------------------- */ + local.outputFile = arguments.options.output; + // Generate final output path + local.outputFile = resolveDumpOutputPath(arguments.options, local.resolvedPath); + detailOutput.statusInfo("Export file: " & local.outputFile); + + // If no output file specified, generate default .sql filename + if (!Len(local.outputFile)) { + local.timestamp = DateFormat(Now(), "yyyymmdd") & TimeFormat(Now(), "HHmmss"); + local.dbName = ListLast(local.resolvedPath, "/\"); + if (ListLen(local.dbName, ".") > 1) { + local.dbName = ListDeleteAt(local.dbName, ListLen(local.dbName, "."), "."); + } + local.outputFile = fileSystemUtil.resolvePath("sqlite_" & local.dbName & "_" & local.timestamp & ".sql"); + } else { + local.outputFile = fileSystemUtil.resolvePath(local.outputFile); + } + + + detailOutput.statusInfo("Export file: #local.outputFile#"); + + /* ---------------------------------------------------- + 3. Check for sqlite3 command line tool + ---------------------------------------------------- */ + local.useSqlite3 = false; + local.sqlite3Result = ""; + + if (isWindows()) { + local.checkCmd = ["where", "sqlite3"]; + } else { + local.checkCmd = ["which", "sqlite3"]; + } + + local.checkResult = runLocalCommand(local.checkCmd, false); + + if (local.checkResult.success) { + detailOutput.statusSuccess("Found sqlite3, attempting native dump..."); + local.useSqlite3 = true; + + // Build sqlite3 command + local.cmdArray = ["sqlite3"]; + + // Add database file path (properly quoted) + local.dbPath = local.resolvedPath; + if (isWindows()) { + // Windows paths with spaces need special handling + if (Find(" ", local.dbPath)) { + local.dbPath = Chr(34) & local.dbPath & Chr(34); + } + } else { + // Unix paths: escape spaces and special characters + local.dbPath = Replace(local.dbPath, "'", "'\''", "all"); + if (Find(" ", local.dbPath) || Find("(", local.dbPath) || Find(")", local.dbPath) || Find("&", local.dbPath)) { + local.dbPath = "'" & local.dbPath & "'"; + } + } + + ArrayAppend(local.cmdArray, local.dbPath); + + // Build the dump command + local.dumpCommands = ""; + + if (arguments.options.schemaOnly) { + local.dumpCommands &= ".schema"; + } else if (arguments.options.dataOnly) { + // For data only, we need to generate INSERT statements + local.dumpCommands &= ".mode insert" & Chr(10); + + // Get list of tables + local.tablesCmd = ArrayDuplicate(local.cmdArray); + ArrayAppend(local.tablesCmd, ".tables"); + local.tablesResult = runLocalCommand(local.tablesCmd, false); + + if (local.tablesResult.success && Len(Trim(local.tablesResult.output))) { + local.tableList = ListToArray(Trim(local.tablesResult.output), " "); + for (local.table in local.tableList) { + local.dumpCommands &= "SELECT * FROM " & local.table & ";" & Chr(10); + } + } + } else { + // Full dump: schema and data + local.dumpCommands &= ".dump" & Chr(10); + } + + // Add table filter if specified + if (Len(arguments.options.tables)) { + local.tableList = ListToArray(arguments.options.tables); + local.dumpCommands = ""; // Reset + + if (!arguments.options.dataOnly) { + // Get schema for specified tables + for (local.table in local.tableList) { + local.dumpCommands &= ".schema " & local.table & Chr(10); + } + } + + if (!arguments.options.schemaOnly) { + // Get data for specified tables + local.dumpCommands &= ".mode insert" & Chr(10); + for (local.table in local.tableList) { + local.dumpCommands &= "SELECT * FROM " & local.table & ";" & Chr(10); + } + } + } + + // Add commands to exit sqlite3 + local.dumpCommands &= ".exit" & Chr(10); + + // Write commands to temporary file + local.tempFile = GetTempDirectory() & "sqlite_dump_" & CreateUUID() & ".txt"; + FileWrite(local.tempFile, local.dumpCommands); + + detailOutput.statusInfo("Executing SQLite dump..."); + + if (isWindows()) { + // Use shell redirection on Windows + local.shellCmd = 'sqlite3 "' & local.resolvedPath & '" < "' & local.tempFile & '"'; + + local.finalCmd = [ + "cmd", "/c", local.shellCmd + ]; + } else { + // Unix-like systems can redirect directly + local.finalCmd = [ + "sh", "-c", + 'sqlite3 "' & local.resolvedPath & '" < "' & local.tempFile & '"' + ]; + } + + local.sqlite3Result = runLocalCommand(local.finalCmd, true); + + + // Clean up temp file + if (FileExists(local.tempFile)) { + try { + FileDelete(local.tempFile); + } catch (any e) { + // Ignore + } + } + + + if (local.sqlite3Result.success) { + // Write output to file + if (Len(local.sqlite3Result.output)) { + FileWrite(local.outputFile, local.sqlite3Result.output); + + // Handle compression if requested + if (arguments.options.compress) { + detailOutput.output("Compressing output file..."); + if (compressFile(local.outputFile)) { + local.outputFile &= ".gz"; + detailOutput.statusSuccess("File compressed: " & local.outputFile); + } else { + detailOutput.statusWarning("Compression failed"); + } + } + + local.fileSize = GetFileInfo(local.outputFile).size; + detailOutput.statusSuccess("SQLite dump completed successfully"); + detailOutput.statusSuccess("File size: " & NumberFormat(local.fileSize / 1024, "0.00") & " KB"); + + return true; + } else { + detailOutput.statusWarning("SQLite dump produced no output"); + return false; + } + } else { + detailOutput.statusWarning("sqlite3 command failed"); + if (Len(local.sqlite3Result.output)) { + detailOutput.output("Error: " & local.sqlite3Result.output); + } + } + } else { + detailOutput.statusWarning("sqlite3 command not found"); + } + + /* ---------------------------------------------------- + 4. JDBC-based export (fallback) + ---------------------------------------------------- */ + if (!local.useSqlite3 || !local.sqlite3Result.success) { + detailOutput.output("Falling back to JDBC-based export..."); + return dumpSQLiteViaJDBC(arguments.dsInfo, arguments.options, local.resolvedPath); + } + + return false; + + } catch (any e) { + detailOutput.error("SQLite dump error: " & e.message); + if (StructKeyExists(e, "detail")) { + detailOutput.error("Details: " & e.detail); + } + return false; + } + } + + private boolean function dumpSQLiteViaJDBC(required struct dsInfo, required struct options, required string dbPath) { + try { + detailOutput.output("Using JDBC connection for SQLite database export"); + + // Get database connection + local.connResult = getDatabaseConnection(arguments.dsInfo, "SQLite"); + + if (!local.connResult.success) { + detailOutput.error("Failed to connect to SQLite database via JDBC"); + if (Len(local.connResult.error)) { + detailOutput.error(local.connResult.error); + } + return false; + } + + local.conn = local.connResult.connection; + local.output = ""; + local.outputFileHandle = ""; + + try { + // Build SQL dump header + local.output &= "-- SQLite Database Dump" & Chr(10); + local.output &= "-- Generated by Wheels CLI (JDBC Mode)" & Chr(10); + local.output &= "-- Database file: " & arguments.dbPath & Chr(10); + local.output &= "-- Generation Time: " & DateTimeFormat(Now(), "yyyy-mm-dd HH:nn:ss") & Chr(10); + local.output &= Chr(10); + local.output &= "PRAGMA foreign_keys=OFF;" & Chr(10); + local.output &= "BEGIN TRANSACTION;" & Chr(10); + local.output &= Chr(10); + + // Get list of tables + local.tableList = []; + if (Len(arguments.options.tables)) { + local.tableList = ListToArray(arguments.options.tables); + } else { + // Get all tables + local.stmt = local.conn.createStatement(); + local.rs = local.stmt.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); + while (local.rs.next()) { + ArrayAppend(local.tableList, local.rs.getString("name")); + } + local.rs.close(); + local.stmt.close(); + } + + detailOutput.statusInfo("Tables to export: " & ArrayLen(local.tableList)); + + // Track progress + local.tableCount = 0; + local.totalRows = 0; + + // Ensure output directory exists + local.outputDir = GetDirectoryFromPath( arguments.options.output ); + if ( Len( local.outputDir ) && !DirectoryExists( local.outputDir ) ) { + DirectoryCreate( local.outputDir, true ); + } + + // Create output file + local.outputFileHandle = FileOpen( + arguments.options.output, + "write", + "utf-8" + ); + FileWrite(local.outputFileHandle, local.output); + local.output = ""; + + // Process each table + for (local.table in local.tableList) { + local.tableCount++; + detailOutput.statusInfo("Exporting table " & local.tableCount & "/" & ArrayLen(local.tableList) & ": " & local.table); + + if (!arguments.options.dataOnly) { + // Get CREATE TABLE statement + local.stmt = local.conn.createStatement(); + local.rs = local.stmt.executeQuery("SELECT sql FROM sqlite_master WHERE type='table' AND name='" & local.table & "'"); + + if (local.rs.next()) { + local.ddl = local.rs.getString("sql"); + if (Len(local.ddl)) { + FileWrite(local.outputFileHandle, "-- Table: " & local.table & Chr(10)); + FileWrite(local.outputFileHandle, "DROP TABLE IF EXISTS " & local.table & ";" & Chr(10)); + FileWrite(local.outputFileHandle, local.ddl & ";" & Chr(10)); + + // Get CREATE INDEX statements + local.stmt2 = local.conn.createStatement(); + local.rs2 = local.stmt2.executeQuery("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name='" & local.table & "' AND sql IS NOT NULL"); + + while (local.rs2.next()) { + local.indexDDL = local.rs2.getString("sql"); + if (Len(local.indexDDL)) { + FileWrite(local.outputFileHandle, local.indexDDL & ";" & Chr(10)); + } + } + local.rs2.close(); + local.stmt2.close(); + } + } + local.rs.close(); + local.stmt.close(); + } + + if (!arguments.options.schemaOnly) { + // Export data + local.stmt = local.conn.createStatement(); + local.countRs = local.stmt.executeQuery("SELECT COUNT(*) as rowcount FROM " & local.table); + local.rowCount = 0; + if (local.countRs.next()) { + local.rowCount = local.countRs.getInt("rowcount"); + } + local.countRs.close(); + local.stmt.close(); + + if (local.rowCount > 0) { + detailOutput.output(" Exporting " & local.rowCount & " rows..."); + + // Get column information + local.stmt = local.conn.createStatement(); + local.rs = local.stmt.executeQuery("PRAGMA table_info('" & local.table & "')"); + + local.columns = []; + while (local.rs.next()) { + ArrayAppend(local.columns, local.rs.getString("name")); + } + local.rs.close(); + local.stmt.close(); + + // Export data in batches + local.batchSize = 1000; + local.offset = 0; + local.batchCount = 0; + + while (local.offset < local.rowCount) { + local.stmt = local.conn.createStatement(); + local.sql = "SELECT * FROM " & local.table; + + // Use LIMIT and OFFSET for batching + if (local.rowCount > local.batchSize) { + local.sql &= " LIMIT " & local.batchSize & " OFFSET " & local.offset; + } + + local.rs = local.stmt.executeQuery(local.sql); + + while (local.rs.next()) { + local.insertStmt = "INSERT INTO " & local.table & " VALUES("; + + for (local.i = 1; local.i <= ArrayLen(local.columns); local.i++) { + if (local.i > 1) local.insertStmt &= ", "; + + try { + local.value = local.rs.getString(local.i); + local.columnType = local.rs.getMetaData().getColumnTypeName(local.i); + + if (IsNull(local.value) || local.rs.wasNull()) { + local.insertStmt &= "NULL"; + } else if (FindNoCase("INT", local.columnType) || FindNoCase("REAL", local.columnType) || FindNoCase("NUMERIC", local.columnType)) { + // Numbers don't need quotes + local.insertStmt &= local.value; + } else if (FindNoCase("BLOB", local.columnType)) { + // Handle BLOB data (hex format for SQLite) + local.insertStmt &= "X'" & ToBase64(local.value) & "'"; + } else { + // Escape single quotes + local.value = Replace(local.value, "'", "''", "all"); + local.insertStmt &= "'" & local.value & "'"; + } + } catch (any e) { + // If there's an error getting the value, use NULL + local.insertStmt &= "NULL"; + } + } + local.insertStmt &= ");"; + + FileWrite(local.outputFileHandle, local.insertStmt & Chr(10)); + local.batchCount++; + local.totalRows++; + } + + local.rs.close(); + local.stmt.close(); + + local.offset += local.batchSize; + + // Show progress for large tables + if (local.rowCount > 10000 && local.offset % 10000 == 0) { + detailOutput.output(" Progress: " & local.offset & "/" & local.rowCount & " rows"); + } + } + } + } + + FileWrite(local.outputFileHandle, Chr(10)); + } + + // Add footer + local.footer = Chr(10) & "COMMIT;" & Chr(10); + local.footer &= "-- Dump completed on " & DateTimeFormat(Now(), "yyyy-mm-dd HH:nn:ss") & Chr(10); + local.footer &= "-- Total tables: " & ArrayLen(local.tableList) & Chr(10); + if (!arguments.options.schemaOnly) { + local.footer &= "-- Total rows exported: " & local.totalRows & Chr(10); + } + + FileWrite(local.outputFileHandle, local.footer); + + // // Close file + FileClose(local.outputFileHandle); + + // Handle compression if requested + if (arguments.options.compress) { + detailOutput.output("Compressing output file..."); + if (compressFile(arguments.options.output)) { + detailOutput.statusSuccess("File compressed successfully"); + } else { + detailOutput.statusWarning("Compression failed"); + } + } + + detailOutput.statusSuccess("SQLite dump completed successfully via JDBC"); + detailOutput.statusInfo("Exported: " & ArrayLen(local.tableList) & " tables, " & local.totalRows & " rows"); + detailOutput.statusInfo("Output file: " & arguments.options.output); + + // Show file size + try { + local.fileInfo = GetFileInfo(arguments.options.output); + local.sizeInMB = NumberFormat(local.fileInfo.size / 1048576, "0.00"); + detailOutput.statusInfo("File Size: #local.sizeInMB# MB"); + } catch (any e) { + // Ignore + } + + return true; + + } catch (any e) { + detailOutput.error("SQLite JDBC export error: " & e.message); + if (StructKeyExists(e, "detail")) { + detailOutput.error("Detail: " & e.detail); + } + + // Close file if open + if (IsDefined("local.outputFileHandle") && IsSimpleValue(local.outputFileHandle)) { + try { + FileClose(local.outputFileHandle); + } catch (any e2) { + // Ignore + } + } + + return false; + } + + } catch (any e) { + detailOutput.error("SQLite JDBC export error: " & e.message); + if (StructKeyExists(e, "detail")) { + detailOutput.error("Details: " & e.detail); + } + return false; + } + } + + private string function resolveDumpOutputPath(required struct options, required string dbFilePath) { + // 1. Use user-provided output if exists + if (Len(arguments.options.output)) { + local.outPath = ExpandPath(arguments.options.output); + } else { + // 2. Default: wheels root + sqlite__.sql + local.appRoot = getCWD(); + local.timestamp = DateFormat(Now(), "yyyymmdd") & TimeFormat(Now(), "HHmmss"); + local.dbName = ListLast(arguments.dbFilePath, "/\"); + if (ListLen(local.dbName, ".") > 1) { + local.dbName = ListDeleteAt(local.dbName, ListLen(local.dbName, "."), "."); + } + local.outPath = local.appRoot & "/sqlite_" & local.dbName & "_" & local.timestamp & ".sql"; + } + + // 3. Ensure output directory exists + local.outDir = GetDirectoryFromPath(local.outPath); + if (!DirectoryExists(local.outDir)) { + DirectoryCreate(local.outDir, true); + } + + // 4. Return canonical absolute path + return ExpandPath(local.outPath); + } + + + // Helper function to duplicate an array + private array function ArrayDuplicate(required array source) { + local.result = []; + for (local.item in arguments.source) { + ArrayAppend(local.result, local.item); + } + return local.result; + } + + // Helper function to compress a file using gzip + private boolean function compressFile(required string filePath) { + try { + if (!FileExists(arguments.filePath)) { + return false; + } + + local.sourceFile = CreateObject("java", "java.io.File").init(arguments.filePath); + local.gzipFile = CreateObject("java", "java.io.File").init(arguments.filePath & ".gz"); + + local.sourceStream = CreateObject("java", "java.io.FileInputStream").init(local.sourceFile); + local.gzipStream = CreateObject("java", "java.io.FileOutputStream").init(local.gzipFile); + local.gzip = CreateObject("java", "java.util.zip.GZIPOutputStream").init(local.gzipStream); + + local.buffer = CreateObject("java", "java.lang.reflect.Array").newInstance( + CreateObject("java", "java.lang.Byte").TYPE, + 1024 + ); + + local.length = 0; + while ((local.length = local.sourceStream.read(local.buffer)) >= 0) { + local.gzip.write(local.buffer, 0, local.length); + } + + local.sourceStream.close(); + local.gzip.close(); + + // Delete original file if compression succeeded + if (gzipFile.length() > 0) { + FileDelete(arguments.filePath); + return true; + } + + return false; + + } catch (any e) { + detailOutput.error("Compression error: " & e.message); + return false; + } + } + private struct function executeSystemCommand(required string command, struct envVars = {}, boolean liveOutput = false) { local.runtime = CreateObject("java", "java.lang.Runtime").getRuntime(); local.isWin = isWindows();