Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions bridge/cgo_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ package bridge
// Forward declarations for Go callbacks
extern int go_local_read_callback(void* user_data, uint8_t* buffer, int size);
extern int go_local_write_callback(void* user_data, uint8_t* buffer, int size);

// Helper function to execute VACUUM on a database
static int execute_vacuum(const char* db_path) {
sqlite3 *db;
char *err_msg = NULL;
int rc;

rc = sqlite3_open(db_path, &db);
if (rc != SQLITE_OK) {
return rc;
}

rc = sqlite3_exec(db, "VACUUM", NULL, NULL, &err_msg);
if (err_msg) {
sqlite3_free(err_msg);
}

sqlite3_close(db);
return rc;
}
*/
import "C"
import (
Expand Down Expand Up @@ -330,3 +350,19 @@ func cgoRunDirectSync(originDbPath, replicaDbPath string, dryRun bool, verbose i

return nil
}

// ExecuteVacuum runs VACUUM on the specified database
func ExecuteVacuum(dbPath string) error {
cDbPath := C.CString(dbPath)
defer C.free(unsafe.Pointer(cDbPath))

result := C.execute_vacuum(cDbPath)
if result != 0 {
return &SQLiteRsyncError{
Code: int(result),
Message: "VACUUM failed",
}
}

return nil
}
4 changes: 3 additions & 1 deletion client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
replicaID string
logger *zap.Logger
showVersion bool
vacuum bool
)

var MAX_MESSAGE_SIZE = 4096
Expand Down Expand Up @@ -162,6 +163,7 @@ func runSync(cmd *cobra.Command, args []string) error {
DryRun: dryRun,
Logger: logger,
Verbose: verbose,
Vacuum: vacuum,
})

// Execute the operation
Expand Down Expand Up @@ -276,7 +278,7 @@ func init() {
rootCmd.Flags().BoolVar(&SetPublic, "public", false, "Enable public access to the replica (initial PUSH only)")
rootCmd.Flags().BoolVar(&dryRun, "dry", false, "Perform a dry run without making changes")
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")

rootCmd.Flags().BoolVar(&vacuum, "vacuum", false, "VACUUM database into temp file before PUSH (optimizes and compacts)")
}

func main() {
Expand Down
79 changes: 77 additions & 2 deletions client/sync/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type CoordinatorConfig struct {
DryRun bool
Logger *zap.Logger
Verbose bool
Vacuum bool
}

// Coordinator manages sync operations and subscriptions
Expand Down Expand Up @@ -439,6 +440,22 @@ func (c *Coordinator) executePush() error {
return fmt.Errorf("database file does not exist: %s", c.config.LocalPath)
}

// Handle --vacuum flag: create vacuumed temp file and use it for push
var actualPushPath string
var cleanupVacuum func()
if c.config.Vacuum {
fmt.Println("🗜️ Creating vacuumed copy for PUSH...")
tempPath, cleanup, err := c.vacuumDatabase(c.config.LocalPath)
if err != nil {
return fmt.Errorf("vacuum failed: %w", err)
}
actualPushPath = tempPath
cleanupVacuum = cleanup
defer cleanupVacuum()
} else {
actualPushPath = c.config.LocalPath
}

// Resolve authentication
authResult, err := c.resolveAuth("push")
if err != nil {
Expand All @@ -457,9 +474,9 @@ func (c *Coordinator) executePush() error {
remotePath = c.config.RemotePath
}

// Create local client for SQLite operations
// Create local client for SQLite operations (using actualPushPath which may be vacuumed)
localClient, err := bridge.New(&bridge.BridgeConfig{
DatabasePath: c.config.LocalPath,
DatabasePath: actualPushPath,
DryRun: c.config.DryRun,
Logger: c.logger.Named("local"),
EnableSQLiteRsyncLogging: c.config.Verbose,
Expand Down Expand Up @@ -653,6 +670,64 @@ func (c *Coordinator) executeLocalSync() error {
return nil
}

// vacuumDatabase creates a vacuumed copy of the database in a temp file
func (c *Coordinator) vacuumDatabase(sourcePath string) (string, func(), error) {
// Get source database size
sourceInfo, err := os.Stat(sourcePath)
if err != nil {
return "", nil, fmt.Errorf("failed to stat source database: %w", err)
}
sourceSize := sourceInfo.Size()

// Check available disk space in /tmp
var stat syscall.Statfs_t
if err := syscall.Statfs("/tmp", &stat); err != nil {
return "", nil, fmt.Errorf("failed to check disk space: %w", err)
}
availableSpace := int64(stat.Bavail * uint64(stat.Bsize))

// Ensure at least 16KB will remain after copy
requiredSpace := sourceSize + 16384
if availableSpace < requiredSpace {
return "", nil, fmt.Errorf("insufficient disk space: need %d bytes, only %d available", requiredSpace, availableSpace)
}

// Create temp file with pattern /tmp/sqlrsync-temp-*
tempFile, err := os.CreateTemp("", "sqlrsync-temp-*.sqlite")
if err != nil {
return "", nil, fmt.Errorf("failed to create temp file: %w", err)
}
tempPath := tempFile.Name()
tempFile.Close()

// Copy source database to temp location
sourceData, err := os.ReadFile(sourcePath)
if err != nil {
os.Remove(tempPath)
return "", nil, fmt.Errorf("failed to read source database: %w", err)
}

if err := os.WriteFile(tempPath, sourceData, 0644); err != nil {
os.Remove(tempPath)
return "", nil, fmt.Errorf("failed to write temp database: %w", err)
}

// Execute VACUUM on the temp file using bridge
if err := bridge.ExecuteVacuum(tempPath); err != nil {
os.Remove(tempPath)
return "", nil, fmt.Errorf("VACUUM execution failed: %w", err)
}

// Return temp path and cleanup function
cleanup := func() {
if err := os.Remove(tempPath); err != nil {
c.logger.Warn("Failed to remove temp vacuum file", zap.String("path", tempPath), zap.Error(err))
}
}

return tempPath, cleanup, nil
}

// Close cleanly shuts down the coordinator
func (c *Coordinator) Close() error {
c.cancel()
Expand Down