# PostgreSQL Backup and Restore Analysis
## Summary
**Finding**: Jellyfin does **NOT use pg_dump/pg_restore** for PostgreSQL backups. Instead, it uses a **generic backup mechanism** that works across all database providers.
**Date**: 2025-01-15
**Analysis**: Migration backup and restore functionality
---
## Current Implementation
### Backup Strategy
Jellyfin uses a **provider-agnostic backup system** that:
1. **SQLite**: File-based backup (copies `jellyfin.db` file)
2. **PostgreSQL**: **No native backup** - Warns users to use pg_dump externally
3. **Generic Backup**: Uses `BackupService` with JSON serialization
---
## PostgreSQL Implementation Analysis
### MigrationBackupFast (PostgresDatabaseProvider.cs)
**Lines 395-403:**
```csharp
///
public async Task MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.");
// Return a key for compatibility, but actual backup should be done externally
return await Task.FromResult(key).ConfigureAwait(false);
}
```
**❌ NO pg_dump usage** - Just logs a warning and returns a dummy key
### RestoreBackupFast (PostgresDatabaseProvider.cs)
**Lines 406-410:**
```csharp
///
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
return Task.CompletedTask;
}
```
**❌ NO pg_restore usage** - Just logs a warning
### DeleteBackup (PostgresDatabaseProvider.cs)
**Lines 413-417:**
```csharp
///
public Task DeleteBackup(string key)
{
logger.LogWarning("Backup deletion is not implemented for PostgreSQL. Manage backups externally.");
return Task.CompletedTask;
}
```
**❌ NO backup management** - Assumes external backup management
---
## Comparison: SQLite vs PostgreSQL
### SQLite Implementation (Working)
**MigrationBackupFast:**
```csharp
public Task MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var path = Path.Combine(applicationPaths.DataPath, "jellyfin.db");
var backupFile = Path.Combine(applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
File.Copy(path, backupFile); // ✅ Simple file copy works
return Task.FromResult(key);
}
```
**RestoreBackupFast:**
```csharp
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
SqliteConnection.ClearAllPools();
var path = Path.Combine(applicationPaths.DataPath, "jellyfin.db");
var backupFile = Path.Combine(applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
File.Copy(backupFile, path, true); // ✅ Restore by copying back
return Task.CompletedTask;
}
```
### PostgreSQL Implementation (Not Implemented)
**MigrationBackupFast:**
```csharp
public async Task MigrationBackupFast(CancellationToken cancellationToken)
{
logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.");
return await Task.FromResult(key).ConfigureAwait(false); // ❌ Does nothing
}
```
**RestoreBackupFast:**
```csharp
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
return Task.CompletedTask; // ❌ Does nothing
}
```
---
## Generic Backup System (BackupService.cs)
Jellyfin has a **separate generic backup system** that works for both SQLite and PostgreSQL:
### CreateBackupAsync (BackupService.cs)
**Process:**
1. Runs database optimization (`VACUUM ANALYZE` for PostgreSQL)
2. Creates a ZIP archive
3. Backs up configuration files
4. **Serializes all database tables to JSON** (no pg_dump)
5. Backs up metadata, trickplay, subtitles (optional)
**Code (lines 260-400+):**
```csharp
public async Task CreateBackupAsync(BackupOptionsDto backupOptions)
{
// ... optimization
await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken);
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create))
{
var dbContext = await _dbProvider.CreateDbContextAsync();
await using (dbContext)
{
// For each entity type
foreach (var entityType in entityTypes)
{
// Serialize entities to JSON
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
await foreach (var item in set)
{
using var document = JsonSerializer.SerializeToDocument(item);
document.WriteTo(jsonSerializer);
}
}
}
// Copy config/data folders
CopyDirectory("Config", ...);
CopyDirectory("Data", ...);
}
}
```
**❌ NOT using pg_dump** - Uses JSON serialization instead
### RestoreBackupAsync (BackupService.cs)
**Process:**
1. Extracts ZIP archive
2. Reads manifest
3. Restores configuration files
4. **Deserializes JSON back to database** (no pg_restore)
5. Restores migration history manually
**Code (lines 87-246):**
```csharp
public async Task RestoreBackupAsync(string archivePath)
{
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);
var dbContext = await _dbProvider.CreateDbContextAsync();
await using (dbContext)
{
// Purge database tables
await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames);
// Restore each table from JSON
foreach (var entityType in entityTypes)
{
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable(zipEntryStream))
{
var entity = item.Deserialize(entityType);
dbContext.Add(entity);
}
}
await dbContext.SaveChangesAsync();
}
}
```
**❌ NOT using pg_restore** - Uses JSON deserialization instead
---
## Why pg_dump/pg_restore Are NOT Used
### Design Philosophy
Jellyfin's backup system is **database-agnostic** to support:
- SQLite (file-based)
- PostgreSQL (server-based)
- Potential future databases (MySQL, SQL Server, etc.)
### Current Approach
- **Generic serialization**: Works with any EF Core provider
- **JSON format**: Human-readable and portable
- **Cross-database**: Can potentially restore SQLite backup to PostgreSQL (with schema migration)
### Limitations of Current Approach
1. **Slow for large databases**: JSON serialization is slower than native tools
2. **No differential backups**: Always full backup
3. **Memory intensive**: Loads entire tables into memory
4. **No compression within JSON**: ZIP compression only
5. **No transaction log**: Point-in-time recovery not possible
---
## PurgeDatabase Implementation
### PostgreSQL (lines 420-446)
```csharp
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable? tableNames)
{
var deleteQueries = new List();
foreach (var tableName in tableNames)
{
var schema = entityType.GetSchema() ?? "public";
deleteQueries.Add($"TRUNCATE TABLE \"{schema}\".\"{tableName}\" CASCADE;");
}
var deleteAllQuery = string.Join('\n', deleteQueries);
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
}
```
**Uses TRUNCATE** - Fast table clearing with CASCADE for foreign keys
### SQLite (lines 193-211)
```csharp
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable? tableNames)
{
var deleteQueries = new List();
foreach (var tableName in tableNames)
{
deleteQueries.Add($"DELETE FROM \"{tableName}\";");
}
var deleteAllQuery =
$"""
PRAGMA foreign_keys = OFF;
{string.Join('\n', deleteQueries)}
PRAGMA foreign_keys = ON;
""";
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
}
```
**Uses DELETE** - SQLite doesn't support TRUNCATE as efficiently
---
## Migration Backup Attribute Usage
### Example from MigrateRatingLevels.cs
```csharp
[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
[JellyfinMigrationBackup(JellyfinDb = true)] // ← Requests backup
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
// Migration code
}
```
**What Happens:**
1. Migration system sees `JellyfinMigrationBackup` attribute
2. Calls `MigrationBackupFast()` on the database provider
3. **For PostgreSQL**: Just logs a warning, no actual backup
4. **For SQLite**: Creates a file-based backup
5. Runs the migration
6. If migration fails, calls `RestoreBackupFast()`
7. **For PostgreSQL**: Logs warning, no actual restore
---
## Recommendations
### 🔴 CRITICAL: PostgreSQL Has No Automatic Backup!
**Current Behavior:**
- Migration attribute `[JellyfinMigrationBackup(JellyfinDb = true)]` does **NOTHING** for PostgreSQL
- Users are at risk if migrations fail
- Manual pg_dump is required before starting Jellyfin
### ✅ Recommendation 1: Implement pg_dump Integration
**Add to PostgresDatabaseProvider.cs:**
```csharp
public async Task MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.backup");
// Get connection details from DbContextFactory
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
var connectionString = context.Database.GetConnectionString();
var builder = new NpgsqlConnectionStringBuilder(connectionString);
// Use pg_dump for native PostgreSQL backup
var pgDumpPath = FindPgDumpExecutable() ?? "pg_dump";
var arguments = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} " +
$"-d {builder.Database} -F c -f \"{backupPath}\"";
var processInfo = new ProcessStartInfo
{
FileName = pgDumpPath,
Arguments = arguments,
Environment = { ["PGPASSWORD"] = builder.Password },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(processInfo);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"pg_dump failed with exit code {process.ExitCode}");
}
logger.LogInformation("PostgreSQL backup created at {BackupPath}", backupPath);
return key;
}
}
```
### ✅ Recommendation 2: Implement pg_restore Integration
```csharp
public async Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.backup");
if (!File.Exists(backupPath))
{
logger.LogCritical("Backup file not found: {BackupPath}", backupPath);
return;
}
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
var connectionString = context.Database.GetConnectionString();
var builder = new NpgsqlConnectionStringBuilder(connectionString);
// Drop and recreate database
await DropAndRecreateDatabaseAsync(builder, cancellationToken);
// Use pg_restore
var pgRestorePath = FindPgRestoreExecutable() ?? "pg_restore";
var arguments = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} " +
$"-d {builder.Database} \"{backupPath}\"";
var processInfo = new ProcessStartInfo
{
FileName = pgRestorePath,
Arguments = arguments,
Environment = { ["PGPASSWORD"] = builder.Password },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(processInfo);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"pg_restore failed with exit code {process.ExitCode}");
}
logger.LogInformation("PostgreSQL backup restored from {BackupPath}", backupPath);
}
}
```
### ✅ Recommendation 3: Alternative - Use SQL Dump Format
**Simpler approach** without requiring pg_dump/pg_restore binaries:
```csharp
public async Task MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.sql");
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
// Use PostgreSQL COPY command to export data
foreach (var schema in Schemas.All)
{
var tables = context.Model.GetEntityTypes()
.Where(et => et.GetSchema() == schema)
.Select(et => et.GetTableName());
foreach (var table in tables)
{
// Export using COPY TO
var query = $"COPY \"{schema}\".\"{table}\" TO STDOUT WITH (FORMAT CSV, HEADER true)";
// Save to backup file
}
}
}
return key;
}
```
---
## Risk Analysis
### Current Risks with PostgreSQL Migrations
| Risk | Severity | Impact | Mitigation |
|------|----------|--------|------------|
| **No automatic backup** | 🔴 HIGH | Data loss if migration fails | Users must manually backup |
| **No rollback capability** | 🔴 HIGH | Cannot undo failed migrations | Manual pg_restore required |
| **Migration failure** | 🟡 MEDIUM | Database corruption possible | Transaction rollback helps |
| **User awareness** | 🟡 MEDIUM | Users may not backup | Documentation needed |
### Current Behavior
**If a PostgreSQL migration fails:**
1. ❌ No automatic backup exists
2. ❌ No automatic rollback
3. ❌ Database may be left in inconsistent state
4. ⚠️ User must manually restore from their own pg_dump backup
**For SQLite:**
1. ✅ Automatic file backup created
2. ✅ Can restore backup automatically
3. ✅ Rollback possible
4. ✅ User protected from data loss
---
## Generic Backup System (BackupService)
### How It Works
**Create Backup (lines 260-420):**
1. Optimize database (`VACUUM ANALYZE` for PostgreSQL)
2. Create ZIP archive
3. Serialize all database tables to JSON
4. Include migration history
5. Copy config/data folders
6. Create manifest.json
**Restore Backup (lines 87-246):**
1. Extract ZIP archive
2. Read manifest
3. **Purge all tables** (TRUNCATE CASCADE for PostgreSQL)
4. Restore migration history
5. Deserialize JSON and insert records
6. Restore config/data folders
### Advantages
- ✅ Works with any database provider
- ✅ Human-readable format (JSON)
- ✅ Portable between databases (potentially)
- ✅ Includes all Jellyfin data (config, metadata)
### Disadvantages
- ❌ Slow for large databases (serialization overhead)
- ❌ Memory intensive
- ❌ No differential/incremental backups
- ❌ Not using native database features
- ❌ No point-in-time recovery
---
## Proposed Solutions
### Option 1: Implement Native pg_dump/pg_restore (Recommended)
**Pros:**
- ✅ Fast and efficient
- ✅ Industry-standard PostgreSQL backup
- ✅ Supports large databases
- ✅ Point-in-time recovery possible
- ✅ Compressed backups
**Cons:**
- ❌ Requires pg_dump/pg_restore binaries
- ❌ Platform-dependent (Windows/Linux paths)
- ❌ Requires PostgreSQL client tools installed
- ❌ More complex error handling
**Implementation Effort**: Medium (1-2 days)
### Option 2: Use PostgreSQL SQL Commands (Alternative)
**Pros:**
- ✅ No external dependencies
- ✅ Works through EF Core connection
- ✅ Cross-platform
- ✅ Simpler implementation
**Cons:**
- ❌ Slower than native tools
- ❌ Limited features
- ❌ Still requires custom format
**Implementation Effort**: Low (4-8 hours)
### Option 3: Document Manual Backup (Current)
**Pros:**
- ✅ No implementation required
- ✅ Users have full control
- ✅ Can use their preferred tools
**Cons:**
- ❌ Users may forget to backup
- ❌ No automatic rollback
- ❌ Risk of data loss
**Implementation Effort**: None (just documentation)
---
## Recommendation for Your Project
### Immediate Action (Current Status)
**✅ GOOD NEWS**: The generic `BackupService` **DOES work** with PostgreSQL:
- It serializes all tables to JSON
- It can restore from JSON
- It's just slower than pg_dump
**⚠️ CONCERN**: `MigrationBackupFast` does nothing for PostgreSQL:
- Migration backups are not created
- Rollback is not possible
- Users are at risk during migrations
### Short-term Solution
**Add warning to documentation:**
```
For PostgreSQL users: Before running Jellyfin updates, always run:
pg_dump -h localhost -U jellyfin -d jellyfin -F c -f jellyfin_backup.backup
```
### Long-term Solution (If Needed)
**Implement pg_dump integration** for `MigrationBackupFast`:
1. Detect if pg_dump is available
2. Fall back to generic backup if not
3. Store backups in standard location
4. Implement restore with pg_restore
---
## Code Locations
### Key Files
1. **Interface**: `src\Jellyfin.Database\Jellyfin.Database.Implementations\IJellyfinDatabaseProvider.cs`
- Defines `MigrationBackupFast()`, `RestoreBackupFast()`, `DeleteBackup()`
2. **PostgreSQL Provider**: `src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\PostgresDatabaseProvider.cs`
- Lines 395-417: Backup/restore methods (NOT IMPLEMENTED)
- Lines 420-446: `PurgeDatabase()` using TRUNCATE CASCADE
3. **SQLite Provider**: `src\Jellyfin.Database\Jellyfin.Database.Providers.Sqlite\SqliteDatabaseProvider.cs`
- Lines 145-190: Backup/restore methods (FULLY IMPLEMENTED with file copy)
4. **Generic Backup**: `Jellyfin.Server.Implementations\FullSystemBackup\BackupService.cs`
- Lines 260-420: `CreateBackupAsync()` using JSON serialization
- Lines 87-246: `RestoreBackupAsync()` using JSON deserialization
5. **Backup Attribute**: `Jellyfin.Server\Migrations\JellyfinMigrationBackupAttribute.cs`
- Decorates migrations that need backup
---
## Testing Verification
### To Verify Current Backup System Works
**1. Create a generic backup:**
```csharp
await _backupService.CreateBackupAsync(new BackupOptionsDto
{
Database = true,
Metadata = false,
Trickplay = false,
Subtitles = false
});
```
**2. Check backup location:**
```
/backups/jellyfin-backup-yyyyMMddHHmmss.zip
```
**3. Inspect contents:**
- `manifest.json` - Backup metadata
- `Database/*.json` - Serialized tables
- `Config/` - Configuration files
- `Data/` - Data folder contents
**4. Test restore:**
```csharp
await _backupService.RestoreBackupAsync(backupPath);
```
### To Test Migration Backup (Will NOT Work for PostgreSQL)
**1. Run a migration with backup attribute:**
```csharp
[JellyfinMigrationBackup(JellyfinDb = true)]
```
**2. Check logs:**
```
[WARN] Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.
```
**3. Verify no backup created** in data/backups folder
---
## Conclusion
### Finding Summary
❌ **pg_dump**: NOT used
❌ **pg_restore**: NOT used
✅ **Generic JSON backup**: Available but not triggered by migrations
❌ **Migration backups**: Do not work for PostgreSQL
### Implications
1. **Current**: PostgreSQL users have **no automatic migration rollback**
2. **Generic backup works**: But not used during migrations
3. **Risk**: Data loss if migration fails
4. **Mitigation**: Users must manually backup with pg_dump
### Next Steps
**For Your Project:**
1. ✅ **Fixed the immediate error** (query materialization)
2. ⚠️ **Be aware**: No automatic backup during migrations
3. 📝 **Document**: Users should run pg_dump before updates
4. 🔧 **Consider**: Implementing pg_dump integration (future enhancement)
**For Production:**
1. **Always backup before Jellyfin updates**: `pg_dump -F c -f backup.backup`
2. **Monitor migrations**: Watch logs for issues
3. **Test migrations**: Use test database first
4. **Keep backups**: Regular pg_dump backups recommended
---
**Document Version**: 1.0
**Date**: 2025-01-15
**Analysis**: PostgreSQL backup mechanisms
**Status**: ❌ pg_dump/pg_restore NOT used, ⚠️ Manual backups required