Compare commits

...

3 Commits

Author SHA1 Message Date
88c141467b v5.5.0: Native engine support for cluster backup/restore
All checks were successful
CI/CD / Test (push) Successful in 3m1s
CI/CD / Lint (push) Successful in 1m12s
CI/CD / Integration Tests (push) Successful in 51s
CI/CD / Native Engine Tests (push) Successful in 51s
CI/CD / Build Binary (push) Successful in 43s
CI/CD / Test Release Build (push) Successful in 1m17s
CI/CD / Release Binaries (push) Successful in 10m27s
NEW FEATURES:
- --native flag for cluster backup creates SQL format (.sql.gz) using pure Go
- --native flag for cluster restore uses pure Go engine for .sql.gz files
- Zero external tool dependencies when using native mode
- Single-binary deployment now possible without pg_dump/pg_restore

CLUSTER BACKUP (--native):
- Creates .sql.gz files instead of .dump files
- Uses pgx wire protocol for data export
- Parallel gzip compression with pgzip
- Automatic fallback with --fallback-tools

CLUSTER RESTORE (--native):
- Restores .sql.gz files using pure Go (pgx CopyFrom)
- No psql or pg_restore required
- Automatic detection: native for .sql.gz, pg_restore for .dump

FILES MODIFIED:
- cmd/backup.go: Added --native and --fallback-tools flags
- cmd/restore.go: Added --native and --fallback-tools flags
- internal/backup/engine.go: Native engine path in BackupCluster()
- internal/restore/engine.go: Added restoreWithNativeEngine()
- NATIVE_ENGINE_SUMMARY.md: Complete rewrite with accurate docs
- CHANGELOG.md: v5.5.0 release notes
2026-02-02 19:18:22 +01:00
3d229f4c5e v5.4.6: Fix progress tracking for large database restores
All checks were successful
CI/CD / Test (push) Successful in 3m3s
CI/CD / Lint (push) Successful in 1m13s
CI/CD / Integration Tests (push) Successful in 52s
CI/CD / Native Engine Tests (push) Successful in 51s
CI/CD / Build Binary (push) Successful in 44s
CI/CD / Test Release Build (push) Successful in 1m20s
CI/CD / Release Binaries (push) Successful in 9m40s
CRITICAL FIX:
- Progress only updated after DB completed, not during restore
- For 100GB DB taking 4+ hours, TUI showed 0% the whole time

CHANGES:
- Heartbeat now reports estimated progress every 5s (was 15s text-only)
- Time-based estimation: ~10MB/s throughput, capped at 95%
- TUI shows spinner + elapsed time when byte-level progress unavailable
- Better visual feedback that restore is actively running
2026-02-02 18:51:33 +01:00
da89e18a25 v5.4.5: Fix disk space estimation for cluster archives
All checks were successful
CI/CD / Test (push) Successful in 3m3s
CI/CD / Lint (push) Successful in 1m12s
CI/CD / Integration Tests (push) Successful in 51s
CI/CD / Native Engine Tests (push) Successful in 50s
CI/CD / Build Binary (push) Successful in 44s
CI/CD / Test Release Build (push) Successful in 1m18s
CI/CD / Release Binaries (push) Successful in 10m10s
- Use 1.2x multiplier for cluster .tar.gz (pre-compressed dumps)
- Use 5x multiplier for single .sql.gz files (was 7x)
- New CheckSystemMemoryWithType() for archive-aware estimation
- 119GB archive now estimates ~143GB instead of ~833GB
2026-02-02 18:38:14 +01:00
10 changed files with 618 additions and 129 deletions

View File

@ -5,6 +5,87 @@ All notable changes to dbbackup will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.5.0] - 2026-02-02
### Added
- **🚀 Native Engine Support for Cluster Backup/Restore**
- NEW: `--native` flag for cluster backup creates SQL format (.sql.gz) using pure Go
- NEW: `--native` flag for cluster restore uses pure Go engine for .sql.gz files
- Zero external tool dependencies when using native mode
- Single-binary deployment now possible without pg_dump/pg_restore installed
- **Native Cluster Backup** (`dbbackup backup cluster --native`)
- Creates .sql.gz files instead of .dump files
- Uses pgx wire protocol for data export
- Parallel gzip compression with pgzip
- Automatic fallback to pg_dump if `--fallback-tools` is set
- **Native Cluster Restore** (`dbbackup restore cluster --native --confirm`)
- Restores .sql.gz files using pure Go (pgx CopyFrom)
- No psql or pg_restore required
- Automatic detection: uses native for .sql.gz, pg_restore for .dump
- Fallback support with `--fallback-tools`
### Updated
- **NATIVE_ENGINE_SUMMARY.md** - Complete rewrite with accurate documentation
- Native engine matrix now shows full cluster support with `--native` flag
### Technical Details
- `internal/backup/engine.go`: Added native engine path in BackupCluster()
- `internal/restore/engine.go`: Added `restoreWithNativeEngine()` function
- `cmd/backup.go`: Added `--native` and `--fallback-tools` flags to cluster command
- `cmd/restore.go`: Added `--native` and `--fallback-tools` flags with PreRunE handlers
- Version bumped to 5.5.0 (new feature release)
## [5.4.6] - 2026-02-02
### Fixed
- **CRITICAL: Progress Tracking for Large Database Restores**
- Fixed "no progress" issue where TUI showed 0% for hours during large single-DB restore
- Root cause: Progress only updated after database *completed*, not during restore
- Heartbeat now reports estimated progress every 5 seconds (was 15s, text-only)
- Time-based progress estimation: ~10MB/s throughput assumption
- Progress capped at 95% until actual completion (prevents jumping to 100% too early)
- **Improved TUI Feedback During Long Restores**
- Shows spinner + elapsed time when byte-level progress not available
- Displays "pg_restore in progress (progress updates every 5s)" message
- Better visual feedback that restore is actively running
### Technical Details
- `reportDatabaseProgressByBytes()` now called during restore, not just after completion
- Heartbeat interval reduced from 15s to 5s for more responsive feedback
- TUI gracefully handles `CurrentDBTotal=0` case with activity indicator
## [5.4.5] - 2026-02-02
### Fixed
- **Accurate Disk Space Estimation for Cluster Archives**
- Fixed WARNING showing 836GB for 119GB archive - was using wrong compression multiplier
- Cluster archives (.tar.gz) contain pre-compressed .dump files → now uses 1.2x multiplier
- Single SQL files (.sql.gz) still use 5x multiplier (was 7x, slightly optimized)
- New `CheckSystemMemoryWithType(size, isClusterArchive)` method for accurate estimates
- 119GB cluster archive now correctly estimates ~143GB instead of ~833GB
## [5.4.4] - 2026-02-02
### Fixed
- **TUI Header Separator Fix** - Capped separator length at 40 chars to prevent line overflow on wide terminals
## [5.4.3] - 2026-02-02
### Fixed
- **Bulletproof SIGINT Handling** - Zero zombie processes guaranteed
- All external commands now use `cleanup.SafeCommand()` with process group isolation
- `KillCommandGroup()` sends signals to entire process group (-pgid)
- No more orphaned pg_restore/pg_dump/psql/pigz processes on Ctrl+C
- 16 files updated with proper signal handling
- **Eliminated External gzip Process** - The `zgrep` command was spawning `gzip -cdfq`
- Replaced with in-process pgzip decompression in `preflight.go`
- `estimateBlobsInSQL()` now uses pure Go pgzip.NewReader
- Zero external gzip processes during restore
## [5.1.22] - 2026-02-01
### Added

View File

@ -1,10 +1,49 @@
# Native Database Engine Implementation Summary
## Mission Accomplished: Zero External Tool Dependencies
## Current Status: Full Native Engine Support (v5.5.0+)
**User Goal:** "FULL - no dependency to the other tools"
**Goal:** Zero dependency on external tools (pg_dump, pg_restore, mysqldump, mysql)
**Result:** **COMPLETE SUCCESS** - dbbackup now operates with **zero external tool dependencies**
**Reality:** Native engine is **NOW AVAILABLE FOR ALL OPERATIONS** when using `--native` flag!
## Engine Support Matrix
| Operation | Default Mode | With `--native` Flag |
|-----------|-------------|---------------------|
| **Single DB Backup** | ✅ Native Go | ✅ Native Go |
| **Single DB Restore** | ✅ Native Go | ✅ Native Go |
| **Cluster Backup** | pg_dump (custom format) | ✅ **Native Go** (SQL format) |
| **Cluster Restore** | pg_restore | ✅ **Native Go** (for .sql.gz files) |
### NEW: Native Cluster Operations (v5.5.0)
```bash
# Native cluster backup - creates SQL format dumps, no pg_dump needed!
./dbbackup backup cluster --native
# Native cluster restore - restores .sql.gz files with pure Go, no pg_restore!
./dbbackup restore cluster backup.tar.gz --native --confirm
```
### Format Selection
| Format | Created By | Restored By | Size | Speed |
|--------|------------|-------------|------|-------|
| **SQL** (.sql.gz) | Native Go or pg_dump | Native Go or psql | Larger | Medium |
| **Custom** (.dump) | pg_dump -Fc | pg_restore only | Smaller | Fast (parallel) |
### When to Use Native Mode
**Use `--native` when:**
- External tools (pg_dump/pg_restore) are not installed
- Running in minimal containers without PostgreSQL client
- Building a single statically-linked binary deployment
- Simplifying disaster recovery procedures
**Use default mode when:**
- Maximum backup/restore performance is critical
- You need parallel restore with `-j` option
- Backup size is a primary concern
## Architecture Overview
@ -27,133 +66,201 @@
- Configuration-based engine initialization
- Unified backup orchestration across engines
4. **Advanced Engine Framework** (`internal/engine/native/advanced.go`)
- Extensible options for advanced backup features
- Support for multiple output formats (SQL, Custom, Directory)
- Compression support (Gzip, Zstd, LZ4)
- Performance optimization settings
5. **Restore Engine Framework** (`internal/engine/native/restore.go`)
- Basic restore architecture (implementation ready)
- Options for transaction control and error handling
4. **Restore Engine Framework** (`internal/engine/native/restore.go`)
- Parses SQL statements from backup
- Uses `CopyFrom` for COPY data
- Progress tracking and status reporting
## Configuration
```bash
# SINGLE DATABASE (native is default for SQL format)
./dbbackup backup single mydb # Uses native engine
./dbbackup restore backup.sql.gz --native # Uses native engine
# CLUSTER BACKUP
./dbbackup backup cluster # Default: pg_dump custom format
./dbbackup backup cluster --native # NEW: Native Go, SQL format
# CLUSTER RESTORE
./dbbackup restore cluster backup.tar.gz --confirm # Default: pg_restore
./dbbackup restore cluster backup.tar.gz --native --confirm # NEW: Native Go for .sql.gz files
# FALLBACK MODE
./dbbackup backup cluster --native --fallback-tools # Try native, fall back if fails
```
### Config Defaults
```go
// internal/config/config.go
UseNativeEngine: true, // Native is default for single DB
FallbackToTools: true, // Fall back to tools if native fails
```
## When Native Engine is Used
### ✅ Native Engine for Single DB (Default)
```bash
# Single DB backup to SQL format
./dbbackup backup single mydb
# → Uses native.PostgreSQLNativeEngine.Backup()
# → Pure Go: pgx COPY TO STDOUT
# Single DB restore from SQL format
./dbbackup restore mydb_backup.sql.gz --database=mydb
# → Uses native.PostgreSQLRestoreEngine.Restore()
# → Pure Go: pgx CopyFrom()
```
### ✅ Native Engine for Cluster (With --native Flag)
```bash
# Cluster backup with native engine
./dbbackup backup cluster --native
# → For each database: native.PostgreSQLNativeEngine.Backup()
# → Creates .sql.gz files (not .dump)
# → Pure Go: no pg_dump required!
# Cluster restore with native engine
./dbbackup restore cluster backup.tar.gz --native --confirm
# → For each .sql.gz: native.PostgreSQLRestoreEngine.Restore()
# → Pure Go: no pg_restore required!
```
### External Tools (Default for Cluster, or Custom Format)
```bash
# Cluster backup (default - uses custom format for efficiency)
./dbbackup backup cluster
# → Uses pg_dump -Fc for each database
# → Reason: Custom format enables parallel restore
# Cluster restore (default)
./dbbackup restore cluster backup.tar.gz --confirm
# → Uses pg_restore for .dump files
# → Uses native engine for .sql.gz files automatically!
# Single DB restore from .dump file
./dbbackup restore mydb_backup.dump --database=mydb
# → Uses pg_restore
# → Reason: Custom format binary file
```
## Performance Comparison
| Method | Format | Backup Speed | Restore Speed | File Size | External Tools |
|--------|--------|-------------|---------------|-----------|----------------|
| Native Go | SQL.gz | Medium | Medium | Larger | ❌ None |
| pg_dump/restore | Custom | Fast | Fast (parallel) | Smaller | ✅ Required |
### Recommendation
| Scenario | Recommended Mode |
|----------|------------------|
| No PostgreSQL tools installed | `--native` |
| Minimal container deployment | `--native` |
| Maximum performance needed | Default (pg_dump) |
| Large databases (>10GB) | Default with `-j8` |
| Disaster recovery simplicity | `--native` |
## Implementation Details
### Data Type Handling
- **PostgreSQL**: Proper handling of arrays, JSON, timestamps, binary data
- **MySQL**: Advanced binary data encoding, proper string escaping, type-specific formatting
- **Both**: NULL value handling, numeric precision, date/time formatting
### Native Backup Flow
### Performance Features
- Configurable batch processing (1000-10000 rows per batch)
- I/O streaming with buffered writers
- Memory-efficient row processing
- Connection pooling support
```
User → backupCmd → cfg.UseNativeEngine=true → runNativeBackup()
native.EngineManager.BackupWithNativeEngine()
native.PostgreSQLNativeEngine.Backup()
pgx: COPY table TO STDOUT → SQL file
```
### Output Formats
- **SQL Format**: Standard SQL DDL and DML statements
- **Custom Format**: (Framework ready for PostgreSQL custom format)
- **Directory Format**: (Framework ready for multi-file output)
### Native Restore Flow
### Configuration Integration
- Seamless integration with existing dbbackup configuration system
- New CLI flags: `--native`, `--fallback-tools`, `--native-debug`
- Backward compatibility with all existing options
```
User → restoreCmd → cfg.UseNativeEngine=true → runNativeRestore()
native.EngineManager.RestoreWithNativeEngine()
native.PostgreSQLRestoreEngine.Restore()
Parse SQL → pgx CopyFrom / Exec → Database
```
## Verification Results
### Native Cluster Flow (NEW in v5.5.0)
```
User → backup cluster --native
For each database:
native.PostgreSQLNativeEngine.Backup()
Create .sql.gz file (not .dump)
Package all .sql.gz into tar.gz archive
User → restore cluster --native --confirm
Extract tar.gz → .sql.gz files
For each .sql.gz:
native.PostgreSQLRestoreEngine.Restore()
Parse SQL → pgx CopyFrom → Database
```
### External Tools Flow (Default Cluster)
```
User → restoreClusterCmd → engine.RestoreCluster()
Extract tar.gz → .dump files
For each .dump:
cleanup.SafeCommand("pg_restore", args...)
PostgreSQL restores data
```
## CLI Flags
### Build Status
```bash
$ go build -o dbbackup-complete .
# Builds successfully with zero warnings
--native # Use native engine for backup/restore (works for cluster too!)
--fallback-tools # Fall back to external if native fails
--native-debug # Enable native engine debug logging
```
### Tool Dependencies
```bash
$ ./dbbackup-complete version
# Database Tools: (none detected)
# Confirms zero external tool dependencies
```
## Future Improvements
### CLI Integration
```bash
$ ./dbbackup-complete backup --help | grep native
--fallback-tools Fallback to external tools if native engine fails
--native Use pure Go native engines (no external tools)
--native-debug Enable detailed native engine debugging
# All native engine flags available
```
1. ~~Add SQL format option for cluster backup~~**DONE in v5.5.0**
## Key Achievements
2. **Implement custom format parser in Go**
- Very complex (PostgreSQL proprietary format)
- Would enable native restore of .dump files
### External Tool Elimination
- **Before**: Required `pg_dump`, `mysqldump`, `pg_restore`, `mysql`, etc.
- **After**: Zero external dependencies - pure Go implementation
3. **Add parallel native restore**
- Parse SQL file into table chunks
- Restore multiple tables concurrently
### Protocol-Level Implementation
- **PostgreSQL**: Direct pgx connection with PostgreSQL wire protocol
- **MySQL**: Direct go-sql-driver with MySQL protocol
- **Both**: Native SQL generation without shelling out to external tools
## Summary
### Advanced Features
- Proper data type handling for complex types (binary, JSON, arrays)
- Configurable batch processing for performance
- Support for multiple output formats and compression
- Extensible architecture for future enhancements
| Feature | Default | With `--native` |
|---------|---------|-----------------|
| Single DB backup (SQL) | ✅ Native Go | ✅ Native Go |
| Single DB restore (SQL) | ✅ Native Go | ✅ Native Go |
| Single DB restore (.dump) | pg_restore | pg_restore |
| Cluster backup | pg_dump (.dump) | ✅ **Native Go (.sql.gz)** |
| Cluster restore (.dump) | pg_restore | pg_restore |
| Cluster restore (.sql.gz) | psql | ✅ **Native Go** |
| MySQL backup | ✅ Native Go | ✅ Native Go |
| MySQL restore | ✅ Native Go | ✅ Native Go |
### Production Ready Features
- Connection management and error handling
- Progress tracking and status reporting
- Configuration integration
- Backward compatibility
**Bottom Line:** With `--native` flag, dbbackup can now perform **ALL operations** without external tools, as long as you create native-format backups. This enables single-binary deployment with zero PostgreSQL client dependencies.
### Code Quality
- Clean, maintainable Go code with proper interfaces
- Comprehensive error handling
- Modular architecture for extensibility
- Integration examples and documentation
**Bottom Line:** With `--native` flag, dbbackup can now perform **ALL operations** without external tools, as long as you create native-format backups. This enables single-binary deployment with zero PostgreSQL client dependencies.
## Usage Examples
### Basic Native Backup
```bash
# PostgreSQL backup with native engine
./dbbackup backup --native --host localhost --port 5432 --database mydb
# MySQL backup with native engine
./dbbackup backup --native --host localhost --port 3306 --database myapp
```
### Advanced Configuration
```go
// PostgreSQL with advanced options
psqlEngine, _ := native.NewPostgreSQLAdvancedEngine(config, log)
result, _ := psqlEngine.AdvancedBackup(ctx, output, &native.AdvancedBackupOptions{
Format: native.FormatSQL,
Compression: native.CompressionGzip,
BatchSize: 10000,
ConsistentSnapshot: true,
})
```
## Final Status
**Mission Status:** **COMPLETE SUCCESS**
The user's goal of "FULL - no dependency to the other tools" has been **100% achieved**.
dbbackup now features:
- **Zero external tool dependencies**
- **Native Go implementations** for both PostgreSQL and MySQL
- **Production-ready** data type handling and performance features
- **Extensible architecture** for future database engines
- **Full CLI integration** with existing dbbackup workflows
The implementation provides a solid foundation that can be enhanced with additional features like:
- Parallel processing implementation
- Custom format support completion
- Full restore functionality implementation
- Additional database engine support
**Result:** A completely self-contained, dependency-free database backup solution written in pure Go.
**Bottom Line:** Native engine works for SQL format operations. Cluster operations use external tools because PostgreSQL's custom format provides better performance and features.

View File

@ -34,8 +34,16 @@ Examples:
var clusterCmd = &cobra.Command{
Use: "cluster",
Short: "Create full cluster backup (PostgreSQL only)",
Long: `Create a complete backup of the entire PostgreSQL cluster including all databases and global objects (roles, tablespaces, etc.)`,
Args: cobra.NoArgs,
Long: `Create a complete backup of the entire PostgreSQL cluster including all databases and global objects (roles, tablespaces, etc.).
Native Engine:
--native - Use pure Go native engine (SQL format, no pg_dump required)
--fallback-tools - Fall back to external tools if native engine fails
By default, cluster backup uses PostgreSQL custom format (.dump) for efficiency.
With --native, all databases are backed up in SQL format (.sql.gz) using the
native Go engine, eliminating the need for pg_dump.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runClusterBackup(cmd.Context())
},
@ -113,6 +121,24 @@ func init() {
backupCmd.AddCommand(singleCmd)
backupCmd.AddCommand(sampleCmd)
// Native engine flags for cluster backup
clusterCmd.Flags().Bool("native", false, "Use pure Go native engine (SQL format, no external tools)")
clusterCmd.Flags().Bool("fallback-tools", false, "Fall back to external tools if native engine fails")
clusterCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("native") {
native, _ := cmd.Flags().GetBool("native")
cfg.UseNativeEngine = native
if native {
log.Info("Native engine mode enabled for cluster backup - using SQL format")
}
}
if cmd.Flags().Changed("fallback-tools") {
fallback, _ := cmd.Flags().GetBool("fallback-tools")
cfg.FallbackToTools = fallback
}
return nil
}
// Incremental backup flags (single backup only) - using global vars to avoid initialization cycle
singleCmd.Flags().StringVar(&backupTypeFlag, "backup-type", "full", "Backup type: full or incremental")
singleCmd.Flags().StringVar(&baseBackupFlag, "base-backup", "", "Path to base backup (required for incremental)")

View File

@ -336,6 +336,8 @@ func init() {
restoreSingleCmd.Flags().BoolVar(&restoreDiagnose, "diagnose", false, "Run deep diagnosis before restore to detect corruption/truncation")
restoreSingleCmd.Flags().StringVar(&restoreSaveDebugLog, "save-debug-log", "", "Save detailed error report to file on failure (e.g., /tmp/restore-debug.json)")
restoreSingleCmd.Flags().BoolVar(&restoreDebugLocks, "debug-locks", false, "Enable detailed lock debugging (captures PostgreSQL config, Guard decisions, boost attempts)")
restoreSingleCmd.Flags().Bool("native", false, "Use pure Go native engine (no psql/pg_restore required)")
restoreSingleCmd.Flags().Bool("fallback-tools", false, "Fall back to external tools if native engine fails")
// Cluster restore flags
restoreClusterCmd.Flags().BoolVar(&restoreListDBs, "list-databases", false, "List databases in cluster backup and exit")
@ -363,6 +365,32 @@ func init() {
restoreClusterCmd.Flags().BoolVar(&restoreCreate, "create", false, "Create target database if it doesn't exist (for single DB restore)")
restoreClusterCmd.Flags().BoolVar(&restoreOOMProtection, "oom-protection", false, "Enable OOM protection: disable swap, tune PostgreSQL memory, protect from OOM killer")
restoreClusterCmd.Flags().BoolVar(&restoreLowMemory, "low-memory", false, "Force low-memory mode: single-threaded restore with minimal memory (use for <8GB RAM or very large backups)")
restoreClusterCmd.Flags().Bool("native", false, "Use pure Go native engine for .sql.gz files (no psql/pg_restore required)")
restoreClusterCmd.Flags().Bool("fallback-tools", false, "Fall back to external tools if native engine fails")
// Handle native engine flags for restore commands
for _, cmd := range []*cobra.Command{restoreSingleCmd, restoreClusterCmd} {
originalPreRun := cmd.PreRunE
cmd.PreRunE = func(c *cobra.Command, args []string) error {
if originalPreRun != nil {
if err := originalPreRun(c, args); err != nil {
return err
}
}
if c.Flags().Changed("native") {
native, _ := c.Flags().GetBool("native")
cfg.UseNativeEngine = native
if native {
log.Info("Native engine mode enabled for restore")
}
}
if c.Flags().Changed("fallback-tools") {
fallback, _ := c.Flags().GetBool("fallback-tools")
cfg.FallbackToTools = fallback
}
return nil
}
}
// PITR restore flags
restorePITRCmd.Flags().StringVar(&pitrBaseBackup, "base-backup", "", "Path to base backup file (.tar.gz) (required)")

View File

@ -22,6 +22,7 @@ import (
"dbbackup/internal/cloud"
"dbbackup/internal/config"
"dbbackup/internal/database"
"dbbackup/internal/engine/native"
"dbbackup/internal/fs"
"dbbackup/internal/logger"
"dbbackup/internal/metadata"
@ -542,6 +543,109 @@ func (e *Engine) BackupCluster(ctx context.Context) error {
format := "custom"
parallel := e.cfg.DumpJobs
// USE NATIVE ENGINE if configured
// This creates .sql.gz files using pure Go (no pg_dump)
if e.cfg.UseNativeEngine {
sqlFile := filepath.Join(tempDir, "dumps", name+".sql.gz")
mu.Lock()
e.printf(" Using native Go engine (pure Go, no pg_dump)\n")
mu.Unlock()
// Create native engine for this database
nativeCfg := &native.PostgreSQLNativeConfig{
Host: e.cfg.Host,
Port: e.cfg.Port,
User: e.cfg.User,
Password: e.cfg.Password,
Database: name,
SSLMode: e.cfg.SSLMode,
Format: "sql",
Compression: compressionLevel,
Parallel: e.cfg.Jobs,
Blobs: true,
Verbose: e.cfg.Debug,
}
nativeEngine, nativeErr := native.NewPostgreSQLNativeEngine(nativeCfg, e.log)
if nativeErr != nil {
if e.cfg.FallbackToTools {
mu.Lock()
e.log.Warn("Native engine failed, falling back to pg_dump", "database", name, "error", nativeErr)
e.printf(" [WARN] Native engine failed, using pg_dump fallback\n")
mu.Unlock()
// Fall through to use pg_dump below
} else {
e.log.Error("Failed to create native engine", "database", name, "error", nativeErr)
mu.Lock()
e.printf(" [FAIL] Failed to create native engine for %s: %v\n", name, nativeErr)
mu.Unlock()
atomic.AddInt32(&failCount, 1)
return
}
} else {
// Connect and backup with native engine
if connErr := nativeEngine.Connect(ctx); connErr != nil {
if e.cfg.FallbackToTools {
mu.Lock()
e.log.Warn("Native engine connection failed, falling back to pg_dump", "database", name, "error", connErr)
mu.Unlock()
} else {
e.log.Error("Native engine connection failed", "database", name, "error", connErr)
atomic.AddInt32(&failCount, 1)
nativeEngine.Close()
return
}
} else {
// Create output file with compression
outFile, fileErr := os.Create(sqlFile)
if fileErr != nil {
e.log.Error("Failed to create output file", "file", sqlFile, "error", fileErr)
atomic.AddInt32(&failCount, 1)
nativeEngine.Close()
return
}
// Use pgzip for parallel compression
gzWriter, _ := pgzip.NewWriterLevel(outFile, compressionLevel)
result, backupErr := nativeEngine.Backup(ctx, gzWriter)
gzWriter.Close()
outFile.Close()
nativeEngine.Close()
if backupErr != nil {
os.Remove(sqlFile) // Clean up partial file
if e.cfg.FallbackToTools {
mu.Lock()
e.log.Warn("Native backup failed, falling back to pg_dump", "database", name, "error", backupErr)
e.printf(" [WARN] Native backup failed, using pg_dump fallback\n")
mu.Unlock()
// Fall through to use pg_dump below
} else {
e.log.Error("Native backup failed", "database", name, "error", backupErr)
atomic.AddInt32(&failCount, 1)
return
}
} else {
// Native backup succeeded!
if info, statErr := os.Stat(sqlFile); statErr == nil {
mu.Lock()
e.printf(" [OK] Completed %s (%s) [native]\n", name, formatBytes(info.Size()))
mu.Unlock()
e.log.Info("Native backup completed",
"database", name,
"size", info.Size(),
"duration", result.Duration,
"engine", result.EngineUsed)
}
atomic.AddInt32(&successCount, 1)
return // Skip pg_dump path
}
}
}
}
// Standard pg_dump path (for non-native mode or fallback)
if size, err := e.db.GetDatabaseSize(ctx, name); err == nil {
if size > 5*1024*1024*1024 {
format = "plain"

View File

@ -20,6 +20,7 @@ import (
"dbbackup/internal/cleanup"
"dbbackup/internal/config"
"dbbackup/internal/database"
"dbbackup/internal/engine/native"
"dbbackup/internal/fs"
"dbbackup/internal/logger"
"dbbackup/internal/progress"
@ -533,7 +534,23 @@ func (e *Engine) restorePostgreSQLSQL(ctx context.Context, archivePath, targetDB
return fmt.Errorf("dump validation failed: %w - the backup file may be truncated or corrupted", err)
}
// Use psql for SQL scripts
// USE NATIVE ENGINE if configured
// This uses pure Go (pgx) instead of psql
if e.cfg.UseNativeEngine {
e.log.Info("Using native Go engine for restore", "database", targetDB, "file", archivePath)
nativeErr := e.restoreWithNativeEngine(ctx, archivePath, targetDB, compressed)
if nativeErr != nil {
if e.cfg.FallbackToTools {
e.log.Warn("Native restore failed, falling back to psql", "database", targetDB, "error", nativeErr)
} else {
return fmt.Errorf("native restore failed: %w", nativeErr)
}
} else {
return nil // Native restore succeeded!
}
}
// Use psql for SQL scripts (fallback or non-native mode)
var cmd []string
// For localhost, omit -h to use Unix socket (avoids Ident auth issues)
@ -570,6 +587,69 @@ func (e *Engine) restorePostgreSQLSQL(ctx context.Context, archivePath, targetDB
return e.executeRestoreCommand(ctx, cmd)
}
// restoreWithNativeEngine restores a SQL file using the pure Go native engine
func (e *Engine) restoreWithNativeEngine(ctx context.Context, archivePath, targetDB string, compressed bool) error {
// Create native engine config
nativeCfg := &native.PostgreSQLNativeConfig{
Host: e.cfg.Host,
Port: e.cfg.Port,
User: e.cfg.User,
Password: e.cfg.Password,
Database: targetDB, // Connect to target database
SSLMode: e.cfg.SSLMode,
}
// Create restore engine
restoreEngine, err := native.NewPostgreSQLRestoreEngine(nativeCfg, e.log)
if err != nil {
return fmt.Errorf("failed to create native restore engine: %w", err)
}
defer restoreEngine.Close()
// Open input file
file, err := os.Open(archivePath)
if err != nil {
return fmt.Errorf("failed to open backup file: %w", err)
}
defer file.Close()
var reader io.Reader = file
// Handle compression
if compressed {
gzReader, err := pgzip.NewReader(file)
if err != nil {
return fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzReader.Close()
reader = gzReader
}
// Restore with progress tracking
options := &native.RestoreOptions{
Database: targetDB,
ContinueOnError: true, // Be resilient like pg_restore
ProgressCallback: func(progress *native.RestoreProgress) {
e.log.Debug("Native restore progress",
"operation", progress.Operation,
"objects", progress.ObjectsCompleted,
"rows", progress.RowsProcessed)
},
}
result, err := restoreEngine.Restore(ctx, reader, options)
if err != nil {
return fmt.Errorf("native restore failed: %w", err)
}
e.log.Info("Native restore completed",
"database", targetDB,
"objects", result.ObjectsProcessed,
"duration", result.Duration)
return nil
}
// restoreMySQLSQL restores from MySQL SQL script
func (e *Engine) restoreMySQLSQL(ctx context.Context, archivePath, targetDB string, compressed bool) error {
options := database.RestoreOptions{}
@ -1371,7 +1451,7 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string, preExtr
if statErr == nil && archiveStats != nil {
backupSizeBytes = archiveStats.Size()
}
memCheck := guard.CheckSystemMemory(backupSizeBytes)
memCheck := guard.CheckSystemMemoryWithType(backupSizeBytes, true) // true = cluster archive with pre-compressed dumps
if memCheck != nil {
if memCheck.Critical {
e.log.Error("🚨 CRITICAL MEMORY WARNING", "error", memCheck.Recommendation)
@ -1689,19 +1769,54 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string, preExtr
preserveOwnership := isSuperuser
isCompressedSQL := strings.HasSuffix(dumpFile, ".sql.gz")
// Get expected size for this database for progress estimation
expectedDBSize := dbSizes[dbName]
// Start heartbeat ticker to show progress during long-running restore
// Use 15s interval to reduce mutex contention during parallel restores
// CRITICAL FIX: Report progress to TUI callbacks so large DB restores show updates
heartbeatCtx, cancelHeartbeat := context.WithCancel(ctx)
heartbeatTicker := time.NewTicker(15 * time.Second)
heartbeatTicker := time.NewTicker(5 * time.Second) // More frequent updates (was 15s)
heartbeatCount := int64(0)
go func() {
for {
select {
case <-heartbeatTicker.C:
heartbeatCount++
elapsed := time.Since(dbRestoreStart)
mu.Lock()
statusMsg := fmt.Sprintf("Restoring %s (%d/%d) - elapsed: %s",
dbName, idx+1, totalDBs, formatDuration(elapsed))
e.progress.Update(statusMsg)
// CRITICAL: Report activity to TUI callbacks during long-running restore
// Use time-based progress estimation: assume ~10MB/s average throughput
// This gives visual feedback even when pg_restore hasn't completed
estimatedBytesPerSec := int64(10 * 1024 * 1024) // 10 MB/s conservative estimate
estimatedBytesDone := elapsed.Milliseconds() / 1000 * estimatedBytesPerSec
if expectedDBSize > 0 && estimatedBytesDone > expectedDBSize {
estimatedBytesDone = expectedDBSize * 95 / 100 // Cap at 95%
}
// Calculate current progress including in-flight database
currentBytesEstimate := bytesCompleted + estimatedBytesDone
// Report to TUI with estimated progress
e.reportDatabaseProgressByBytes(currentBytesEstimate, totalBytes, dbName, int(atomic.LoadInt32(&successCount)), totalDBs)
// Also report timing info
phaseElapsed := time.Since(restorePhaseStart)
var avgPerDB time.Duration
completedDBTimesMu.Lock()
if len(completedDBTimes) > 0 {
var total time.Duration
for _, d := range completedDBTimes {
total += d
}
avgPerDB = total / time.Duration(len(completedDBTimes))
}
completedDBTimesMu.Unlock()
e.reportDatabaseProgressWithTiming(idx, totalDBs, dbName, phaseElapsed, avgPerDB)
mu.Unlock()
case <-heartbeatCtx.Done():
return

View File

@ -358,6 +358,14 @@ func (g *LargeDBGuard) WarnUser(strategy *RestoreStrategy, silentMode bool) {
// CheckSystemMemory validates system has enough memory for restore
func (g *LargeDBGuard) CheckSystemMemory(backupSizeBytes int64) *MemoryCheck {
return g.CheckSystemMemoryWithType(backupSizeBytes, false)
}
// CheckSystemMemoryWithType validates system memory with archive type awareness
// isClusterArchive: true for .tar.gz cluster backups (contain pre-compressed .dump files)
//
// false for single .sql.gz files (compressed SQL that expands significantly)
func (g *LargeDBGuard) CheckSystemMemoryWithType(backupSizeBytes int64, isClusterArchive bool) *MemoryCheck {
check := &MemoryCheck{
BackupSizeGB: float64(backupSizeBytes) / (1024 * 1024 * 1024),
}
@ -374,8 +382,18 @@ func (g *LargeDBGuard) CheckSystemMemory(backupSizeBytes int64) *MemoryCheck {
check.SwapTotalGB = float64(memInfo.SwapTotal) / (1024 * 1024 * 1024)
check.SwapFreeGB = float64(memInfo.SwapFree) / (1024 * 1024 * 1024)
// Estimate uncompressed size (typical compression ratio 5:1 to 10:1)
estimatedUncompressedGB := check.BackupSizeGB * 7 // Conservative estimate
// Estimate uncompressed size based on archive type:
// - Cluster archives (.tar.gz): contain pre-compressed .dump files, ratio ~1.2x
// - Single SQL files (.sql.gz): compressed SQL expands significantly, ratio ~5-7x
var compressionMultiplier float64
if isClusterArchive {
compressionMultiplier = 1.2 // tar.gz with already-compressed .dump files
g.log.Debug("Using cluster archive compression ratio", "multiplier", compressionMultiplier)
} else {
compressionMultiplier = 5.0 // Conservative for gzipped SQL (was 7, reduced to 5)
g.log.Debug("Using single file compression ratio", "multiplier", compressionMultiplier)
}
estimatedUncompressedGB := check.BackupSizeGB * compressionMultiplier
// Memory requirements
// - PostgreSQL needs ~2-4GB for shared_buffers

View File

@ -423,7 +423,7 @@ func (e *Engine) estimateBlobsInSQL(sqlFile string) int {
// Scan for lo_create patterns
// We use a regex to match both "lo_create" and "SELECT lo_create" patterns
loCreatePattern := regexp.MustCompile(`lo_create`)
scanner := bufio.NewScanner(gzReader)
// Use larger buffer for potentially long lines
buf := make([]byte, 0, 256*1024)
@ -436,7 +436,7 @@ func (e *Engine) estimateBlobsInSQL(sqlFile string) int {
for scanner.Scan() && linesScanned < maxLines {
line := scanner.Text()
linesScanned++
// Count all lo_create occurrences in the line
matches := loCreatePattern.FindAllString(line, -1)
count += len(matches)

View File

@ -227,11 +227,21 @@ func (v *RichClusterProgressView) renderPhaseDetails(snapshot *progress.Progress
bar := v.renderMiniProgressBar(pct)
phaseElapsed := time.Since(snapshot.PhaseStartTime)
b.WriteString(fmt.Sprintf(" %s %-20s %s %3d%%\n",
spinner, truncateString(snapshot.CurrentDB, 20), bar, pct))
b.WriteString(fmt.Sprintf(" └─ %s / %s (running %s)\n",
FormatBytes(snapshot.CurrentDBBytes), FormatBytes(snapshot.CurrentDBTotal),
formatDuration(phaseElapsed)))
// Better display when we have progress info vs when we're waiting
if snapshot.CurrentDBTotal > 0 {
b.WriteString(fmt.Sprintf(" %s %-20s %s %3d%%\n",
spinner, truncateString(snapshot.CurrentDB, 20), bar, pct))
b.WriteString(fmt.Sprintf(" └─ %s / %s (running %s)\n",
FormatBytes(snapshot.CurrentDBBytes), FormatBytes(snapshot.CurrentDBTotal),
formatDuration(phaseElapsed)))
} else {
// No byte-level progress available - show activity indicator with elapsed time
b.WriteString(fmt.Sprintf(" %s %-20s [restoring...] running %s\n",
spinner, truncateString(snapshot.CurrentDB, 20),
formatDuration(phaseElapsed)))
b.WriteString(fmt.Sprintf(" └─ pg_restore in progress (progress updates every 5s)\n"))
}
}
// Show remaining count

View File

@ -16,7 +16,7 @@ import (
// Build information (set by ldflags)
var (
version = "5.4.4"
version = "5.5.0"
buildTime = "unknown"
gitCommit = "unknown"
)