Last active
March 25, 2025 19:52
-
-
Save blakedietz/37ca34bfb28fb2afb5b2335651d96955 to your computer and use it in GitHub Desktop.
Database snapshot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if test -d ~/.config/fish/functions/work/jump | |
set -p fish_function_path ~/.config/fish/functions/work/jump | |
function db-snapshot | |
/Users/blakedietz/.config/fish/functions/work/jump/snapshots.fish $argv | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env fish | |
# PostgreSQL Database Snapshot Management Script | |
# Commands: | |
# - snapshot [name] [--notes "description"]: Create database snapshots for all DBs with a matching prefix and archive them (uses date/time if no name provided) | |
# - restore: Use fzf to select an archive and restore snapshots from it | |
# - delete: Use fzf to select an archive to delete | |
# Configuration | |
set -g BACKUP_DIR /Users/blakedietz/projects/Jump-App/database-backups | |
set -g ARCHIVE_DIR /Users/blakedietz/projects/Jump-App/database-backups/archives | |
set -g TEMP_DIR /tmp/db-restore | |
set -g METADATA_DB /Users/blakedietz/projects/Jump-App/database-backups/snapshots.db | |
set -g DB_PREFIX jump_ # Prefix for databases to backup/restore | |
set -g DB_USER postgres # Set your database user here | |
set -g DB_HOST localhost # Set your database host here | |
set -g DB_PORT 5432 # Set your database port here | |
set -g DB_PASSWORD postgres # Development password (not sensitive) | |
# Path to PostgreSQL binaries - adjust based on your installation | |
# For Postgres.app, try to use its binaries to avoid version mismatch | |
set -g PG_BIN_PATH "/Applications/Postgres.app/Contents/Versions/latest/bin" | |
function init_metadata_db | |
# Create metadata database if it doesn't exist | |
if not test -f $METADATA_DB | |
sqlite3 $METADATA_DB " | |
CREATE TABLE IF NOT EXISTS snapshots ( | |
name TEXT PRIMARY KEY, | |
timestamp TEXT NOT NULL, | |
notes TEXT, | |
databases TEXT NOT NULL, | |
created_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
);" | |
end | |
end | |
function ensure_dirs | |
for dir in $BACKUP_DIR $ARCHIVE_DIR $TEMP_DIR | |
if not test -d $dir | |
mkdir -p $dir | |
echo "Created directory: $dir" | |
end | |
end | |
init_metadata_db | |
end | |
function get_matching_databases | |
set -l databases (PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -t -c "SELECT datname FROM pg_database WHERE datname LIKE '$DB_PREFIX%';") | |
# Clean up the output (remove whitespace) and filter out empty strings | |
for db in $databases | |
set -l trimmed (string trim $db) | |
if test -n "$trimmed" | |
echo $trimmed | |
end | |
end | |
end | |
function db_snapshot | |
ensure_dirs | |
set timestamp (date +"%Y%m%d_%H%M%S") | |
set -l notes_opt (fish_opt --short=n --long=notes --required-val) | |
argparse $notes_opt -- $argv | |
or return 1 | |
if test (count $argv) -gt 0 | |
set archive_name "$argv[1]" | |
else | |
set archive_name "snapshot_$timestamp" | |
end | |
# If notes weren't provided via flag, prompt for them | |
if not set -q _flag_notes | |
read -P "Enter notes for this snapshot (optional): " _flag_notes | |
end | |
# Get all databases matching the prefix | |
set matching_dbs (get_matching_databases) | |
if test (count $matching_dbs) -eq 0 | |
echo "❌ No databases found with prefix '$DB_PREFIX'" | |
return 1 | |
end | |
echo "Found "(count $matching_dbs)" databases matching prefix '$DB_PREFIX'" | |
# Create a temporary directory for snapshots | |
set temp_snapshot_dir "$TEMP_DIR/$archive_name" | |
mkdir -p $temp_snapshot_dir | |
# Save metadata to SQLite | |
if test -n "$_flag_notes" | |
sqlite3 $METADATA_DB "INSERT INTO snapshots (name, timestamp, notes, databases) VALUES ('$archive_name', '$timestamp', '$_flag_notes', '$(string join ',' $matching_dbs)');" | |
else | |
sqlite3 $METADATA_DB "INSERT INTO snapshots (name, timestamp, databases) VALUES ('$archive_name', '$timestamp', '$(string join ',' $matching_dbs)');" | |
end | |
for db in $matching_dbs | |
set backup_path "$temp_snapshot_dir/$db.sql" | |
echo "Creating snapshot of database '$db': $backup_path" | |
# Use Postgres.app pg_dump instead of Homebrew version | |
set pg_dump_path "$PG_BIN_PATH/pg_dump" | |
# Check if Postgres.app pg_dump exists, otherwise try to use the system one | |
if not test -e $pg_dump_path | |
echo "Warning: Could not find Postgres.app pg_dump at $pg_dump_path" | |
echo "Trying system pg_dump, but version mismatch is likely..." | |
set pg_dump_path pg_dump | |
end | |
PGPASSWORD=$DB_PASSWORD $pg_dump_path -h $DB_HOST -p $DB_PORT -U $DB_USER -d $db -F p >$backup_path | |
if test $status -eq 0 | |
echo "✅ Snapshot of '$db' created successfully!" | |
else | |
echo "❌ Failed to create snapshot of '$db'!" | |
rm -rf $temp_snapshot_dir | |
sqlite3 $METADATA_DB "DELETE FROM snapshots WHERE name = '$archive_name';" | |
return 1 | |
end | |
end | |
# Create archive | |
set archive_path "$ARCHIVE_DIR/$archive_name.tar.gz" | |
tar -czf $archive_path -C $temp_snapshot_dir . | |
if test $status -eq 0 | |
echo "✅ Created archive: $archive_path" | |
test -n "$_flag_notes" && echo "📝 Notes: $_flag_notes" | |
# Clean up temporary files | |
rm -rf $temp_snapshot_dir | |
else | |
echo "❌ Failed to create archive!" | |
rm -rf $temp_snapshot_dir | |
sqlite3 $METADATA_DB "DELETE FROM snapshots WHERE name = '$archive_name';" | |
return 1 | |
end | |
end | |
function db_restore | |
ensure_dirs | |
# Check if fzf is installed | |
if not command -q fzf | |
echo "❌ fzf is not installed. Please install it with 'brew install fzf' or your package manager." | |
return 1 | |
end | |
# Check if there are any archives | |
set archive_count (count $ARCHIVE_DIR/*.tar.gz) | |
if test $archive_count -eq 0 | |
echo "❌ No archives found in $ARCHIVE_DIR" | |
return 1 | |
end | |
# Create a temporary preview script | |
set preview_script (mktemp) | |
echo '#!/usr/bin/env fish | |
set archive_path $argv[1] | |
set archive_name (basename $archive_path .tar.gz) | |
echo "Archive contents:" | |
echo "----------------" | |
tar -tf $archive_path | sed "s/.*\///" | |
echo -e "\nMetadata:" | |
echo "----------------" | |
sqlite3 "'$METADATA_DB'" " | |
SELECT | |
\"Created: \" || datetime(created_at, \"localtime\") || \"\n\" || | |
\"Timestamp: \" || timestamp || \"\n\" || | |
\"Databases: \" || databases || | |
CASE WHEN notes IS NOT NULL THEN \"\nNotes: \" || notes ELSE \"\" END | |
FROM snapshots WHERE name = \"$archive_name\";" | |
' > $preview_script | |
chmod +x $preview_script | |
# Use fzf to select an archive, showing both contents and metadata in preview | |
set selected_archive (find $ARCHIVE_DIR -name "*.tar.gz" -type f | \ | |
sort -r | \ | |
awk -F/ '{print $NF "\t" $0}' | \ | |
fzf --with-nth=1 \ | |
--preview "eval $preview_script {2}" \ | |
--preview-window="right:50%" \ | |
--height 40% \ | |
--layout reverse \ | |
--border | \ | |
cut -f2) | |
# Clean up the preview script | |
rm $preview_script | |
if test -z "$selected_archive" | |
echo "No archive selected, operation cancelled." | |
return 0 | |
end | |
# Show metadata before proceeding | |
set -l archive_name (basename $selected_archive .tar.gz) | |
echo -e "\nSnapshot Information:" | |
echo "--------------------" | |
sqlite3 $METADATA_DB " | |
SELECT | |
\"Created: \" || datetime(created_at, \"localtime\") || \"\n\" || | |
\"Timestamp: \" || timestamp || \"\n\" || | |
\"Databases: \" || databases || | |
CASE WHEN notes IS NOT NULL THEN \"\nNotes: \" || notes ELSE \"\" END | |
FROM snapshots WHERE name = \"$archive_name\";" | |
echo "--------------------" | |
# Create a clean temporary directory | |
rm -rf $TEMP_DIR | |
mkdir -p $TEMP_DIR | |
echo "Selected archive: $selected_archive" | |
echo "Extracting archive..." | |
# Extract the archive | |
tar -xzf $selected_archive -C $TEMP_DIR | |
# Get all SQL files from the extracted archive | |
set snapshot_files $TEMP_DIR/*.sql | |
# Ask if user wants to restore all databases at once | |
read -l -P "Do you want to restore all databases without individual prompts? [y/N] " restore_all | |
for snapshot_file in $snapshot_files | |
# The filename is the database name | |
set db_name (string replace -r '.*/([^/]+)\.sql$' '$1' $snapshot_file) | |
if test -z "$db_name" | |
echo "❌ Could not determine database name from filename: $snapshot_file" | |
continue | |
end | |
echo "Found snapshot for database '$db_name': $snapshot_file" | |
set proceed false | |
# If restore_all is yes, proceed without individual prompts | |
switch $restore_all | |
case Y y | |
set proceed true | |
case '*' | |
read -l -P "Are you sure you want to restore '$db_name'? This will overwrite your current database. [y/N] " confirm | |
switch $confirm | |
case Y y | |
set proceed true | |
end | |
end | |
if test $proceed = true | |
echo "Restoring database '$db_name' from: $snapshot_file" | |
# Get path to psql executable | |
set psql_path "$PG_BIN_PATH/psql" | |
if not test -e $psql_path | |
echo "Warning: Could not find Postgres.app psql at $psql_path" | |
echo "Trying system psql..." | |
set psql_path psql | |
end | |
# Drop and recreate the database | |
PGPASSWORD=$DB_PASSWORD $psql_path -h $DB_HOST -p $DB_PORT -U $DB_USER -c "DROP DATABASE IF EXISTS $db_name WITH (FORCE);" | |
PGPASSWORD=$DB_PASSWORD $psql_path -h $DB_HOST -p $DB_PORT -U $DB_USER -c "CREATE DATABASE $db_name;" | |
# Restore from snapshot | |
PGPASSWORD=$DB_PASSWORD $psql_path -h $DB_HOST -p $DB_PORT -U $DB_USER -d $db_name <$snapshot_file | |
if test $status -eq 0 | |
echo "✅ Database '$db_name' restored successfully!" | |
else | |
echo "❌ Failed to restore database '$db_name'!" | |
end | |
else | |
echo "Restore of '$db_name' cancelled." | |
end | |
end | |
# Clean up temporary files | |
rm -rf $TEMP_DIR | |
echo "✨ Cleanup complete" | |
end | |
function db_delete | |
ensure_dirs | |
# Check if fzf is installed | |
if not command -q fzf | |
echo "❌ fzf is not installed. Please install it with 'brew install fzf' or your package manager." | |
return 1 | |
end | |
# Check if there are any archives | |
set archive_count (count $ARCHIVE_DIR/*.tar.gz) | |
if test $archive_count -eq 0 | |
echo "❌ No archives found in $ARCHIVE_DIR" | |
return 1 | |
end | |
# Create a temporary preview script | |
set preview_script (mktemp) | |
echo '#!/usr/bin/env fish | |
set archive_path $argv[1] | |
set archive_name (basename $archive_path .tar.gz) | |
echo "Archive contents:" | |
echo "----------------" | |
tar -tf $archive_path | sed "s/.*\///" | |
echo -e "\nMetadata:" | |
echo "----------------" | |
sqlite3 "'$METADATA_DB'" " | |
SELECT | |
\"Created: \" || datetime(created_at, \"localtime\") || \"\n\" || | |
\"Timestamp: \" || timestamp || \"\n\" || | |
\"Databases: \" || databases || | |
CASE WHEN notes IS NOT NULL THEN \"\nNotes: \" || notes ELSE \"\" END | |
FROM snapshots WHERE name = \"$archive_name\";" | |
' > $preview_script | |
chmod +x $preview_script | |
# Use fzf to select an archive to delete | |
set selected_archive (find $ARCHIVE_DIR -name "*.tar.gz" -type f | \ | |
sort -r | \ | |
awk -F/ '{print $NF "\t" $0}' | \ | |
fzf --with-nth=1 \ | |
--preview "eval $preview_script {2}" \ | |
--preview-window="right:50%" \ | |
--height 40% \ | |
--layout reverse \ | |
--border | \ | |
cut -f2) | |
# Clean up the preview script | |
rm $preview_script | |
if test -z "$selected_archive" | |
echo "No archive selected, operation cancelled." | |
return 0 | |
end | |
# Show metadata before proceeding | |
set -l archive_name (basename $selected_archive .tar.gz) | |
echo -e "\nSnapshot Information to be deleted:" | |
echo "--------------------" | |
sqlite3 $METADATA_DB " | |
SELECT | |
\"Created: \" || datetime(created_at, \"localtime\") || \"\n\" || | |
\"Timestamp: \" || timestamp || \"\n\" || | |
\"Databases: \" || databases || | |
CASE WHEN notes IS NOT NULL THEN \"\nNotes: \" || notes ELSE \"\" END | |
FROM snapshots WHERE name = \"$archive_name\";" | |
echo "--------------------" | |
read -l -P "Are you sure you want to delete this snapshot? This action cannot be undone. [y/N] " confirm | |
switch $confirm | |
case Y y | |
# Delete the archive file | |
rm $selected_archive | |
if test $status -eq 0 | |
# Remove the metadata from SQLite | |
sqlite3 $METADATA_DB "DELETE FROM snapshots WHERE name = '$archive_name';" | |
echo "✅ Snapshot '$archive_name' deleted successfully!" | |
else | |
echo "❌ Failed to delete archive file!" | |
return 1 | |
end | |
case '*' | |
echo "Deletion cancelled." | |
return 0 | |
end | |
end | |
# Parse command line arguments | |
if test (count $argv) -eq 0 | |
echo "Usage: "(status current-filename)" [snapshot|restore|delete] [name] [--notes \"description\"]" | |
exit 1 | |
end | |
switch $argv[1] | |
case snapshot | |
db_snapshot $argv[2..-1] | |
case restore | |
db_restore | |
case delete | |
db_delete | |
case '*' | |
echo "Unknown command: $argv[1]" | |
echo "Available commands: snapshot, restore, delete" | |
exit 1 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment