From 6ded893d1917574912cf4a1ce669ec7a8c75366d Mon Sep 17 00:00:00 2001 From: Seth Horsley Date: Thu, 7 Sep 2023 14:29:28 -0400 Subject: [PATCH 1/3] wipe_development_database now force wipes db to stop error of users connected to db --- lib/parity/backup.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parity/backup.rb b/lib/parity/backup.rb index 5fffea0..7202297 100644 --- a/lib/parity/backup.rb +++ b/lib/parity/backup.rb @@ -49,7 +49,7 @@ def restore_to_development def wipe_development_database Kernel.system( - "dropdb --if-exists #{development_db} && createdb #{development_db}", + "dropdb --if-exists #{development_db} --force && createdb #{development_db}", ) end From 1692e95f9559756e20a7b333c6e717ff5bb0a6f5 Mon Sep 17 00:00:00 2001 From: Seth Horsley Date: Mon, 25 Aug 2025 20:14:17 +0200 Subject: [PATCH 2/3] Update Ruby version to 3.4.1 and enhance backup functionality to support specific backup IDs. Added logging for restoration processes and updated tests to verify new behavior. --- .ruby-version | 2 +- lib/parity/backup.rb | 55 ++++++++++++++++++++++++++------ lib/parity/environment.rb | 21 +++++++++--- spec/parity/backup_spec.rb | 65 +++++++++++++++++++++++++++++++++++++- 4 files changed, 128 insertions(+), 15 deletions(-) diff --git a/.ruby-version b/.ruby-version index 37c2961..47b322c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.2 +3.4.1 diff --git a/lib/parity/backup.rb b/lib/parity/backup.rb index 7202297..3716000 100644 --- a/lib/parity/backup.rb +++ b/lib/parity/backup.rb @@ -11,6 +11,7 @@ def initialize(args) @from, @to = args.values_at(:from, :to) @additional_args = args[:additional_args] || BLANK_ARGUMENTS @parallelize = args[:parallelize] || false + @backup_id = args[:backup_id] end def restore @@ -25,19 +26,33 @@ def restore private - attr_reader :additional_args, :from, :to, :parallelize + attr_reader :additional_args, :from, :to, :parallelize, :backup_id alias :parallelize? :parallelize + + + def log_restore_info + if backup_id + puts "Restoring from #{from} backup ID: #{backup_id} to #{to}" + else + puts "Restoring from #{from} (latest backup) to #{to}" + end + puts "Starting backup restoration process..." + end + def restore_from_development + log_restore_info reset_remote_database Kernel.system( "heroku pg:push #{development_db} DATABASE_URL --remote #{to} "\ "#{additional_args}", ) + puts "Backup restoration to #{to} completed successfully!" end def restore_to_development + log_restore_info ensure_temp_directory_exists download_remote_backup wipe_development_database @@ -45,6 +60,7 @@ def restore_to_development restore_from_local_temp_backup delete_local_temp_backup delete_rails_production_environment_settings + puts "Backup restoration to #{to} completed successfully!" end def wipe_development_database @@ -77,21 +93,34 @@ def ensure_temp_directory_exists end def download_remote_backup - Kernel.system( - "curl -o tmp/latest.backup \"$(heroku pg:backups:url --remote #{from})\"", - ) + if backup_id + puts "Downloading backup #{backup_id} from #{from}..." + Kernel.system( + "curl -o tmp/#{backup_id}.backup \"$(heroku pg:backups:url #{backup_id} --remote #{from})\"", + ) + else + puts "Downloading latest backup from #{from}..." + Kernel.system( + "curl -o tmp/latest.backup \"$(heroku pg:backups:url --remote #{from})\"", + ) + end end def restore_from_local_temp_backup + puts "Restoring backup to #{development_db}..." + # Filter out --backup-id from additional_args as it's not needed for pg_restore + filtered_args = additional_args.gsub(/--backup-id\s+\S+/, '').strip + backup_filename = backup_id ? "#{backup_id}.backup" : "latest.backup" Kernel.system( - "pg_restore tmp/latest.backup --verbose --no-acl --no-owner "\ + "pg_restore tmp/#{backup_filename} --verbose --no-acl --no-owner "\ "--dbname #{development_db} --jobs=#{processor_cores} "\ - "#{additional_args}", + "#{filtered_args}", ) end def delete_local_temp_backup - Kernel.system("rm tmp/latest.backup") + backup_filename = backup_id ? "#{backup_id}.backup" : "latest.backup" + Kernel.system("rm tmp/#{backup_filename}") end def delete_rails_production_environment_settings @@ -101,11 +130,15 @@ def delete_rails_production_environment_settings end def restore_to_remote_environment + log_restore_info reset_remote_database + # Filter out --backup-id from additional_args as it's handled separately + filtered_args = additional_args.gsub(/--backup-id\s+\S+/, '').strip Kernel.system( "heroku pg:backups:restore #{backup_from} --remote #{to} "\ - "#{additional_args}", + "#{filtered_args}", ) + puts "Backup restoration to #{to} completed successfully!" end def backup_from @@ -113,7 +146,11 @@ def backup_from end def remote_db_backup_url - "heroku pg:backups:url --remote #{from}" + if backup_id + "heroku pg:backups:url #{backup_id} --remote #{from}" + else + "heroku pg:backups:url --remote #{from}" + end end def development_db diff --git a/lib/parity/environment.rb b/lib/parity/environment.rb index f293ff4..95be31b 100644 --- a/lib/parity/environment.rb +++ b/lib/parity/environment.rb @@ -67,12 +67,15 @@ def restore $stdout.puts "Parity does not support restoring backups into your "\ "production environment. Use `--force` to override." else - Backup.new( + backup_args = { from: arguments.first, to: environment, parallelize: parallelize?, additional_args: additional_restore_arguments, - ).restore + } + backup_args[:backup_id] = backup_id? if backup_id? + + Backup.new(backup_args).restore end end @@ -90,9 +93,19 @@ def parallelize? arguments.include?("--parallelize") end + def backup_id? + backup_id_index = arguments.index { |arg| arg == "--backup-id" } + if backup_id_index && backup_id_index + 1 < arguments.length + arguments[backup_id_index + 1] + end + end + def additional_restore_arguments - (arguments.drop(1) - ["--force", "--parallelize"] + - [restore_confirmation_argument]).compact.join(" ") + # Filter out special flags that are handled separately + filtered_args = arguments.drop(1) - ["--force", "--parallelize"] + # Filter out --backup-id as it's handled separately by the Backup class + filtered_args = filtered_args.reject { |arg| arg.start_with?("--backup-id") } + (filtered_args + [restore_confirmation_argument]).compact.join(" ") end def restore_confirmation_argument diff --git a/spec/parity/backup_spec.rb b/spec/parity/backup_spec.rb index 2a5e55b..08e2fbc 100644 --- a/spec/parity/backup_spec.rb +++ b/spec/parity/backup_spec.rb @@ -33,6 +33,37 @@ with(delete_local_temp_backup_command) end + it "restores from a specific backup ID when restoring to development" do + allow(IO).to receive(:read).and_return(database_fixture) + allow(Kernel).to receive(:system) + allow(Etc).to receive(:nprocessors).and_return(number_of_processes) + + Parity::Backup.new( + from: "production", + to: "development", + backup_id: "b001", + ).restore + + expect(Kernel). + to have_received(:system). + with(make_temp_directory_command) + expect(Kernel). + to have_received(:system). + with(specific_backup_id_download_command) + expect(Kernel). + to have_received(:system). + with(drop_development_database_drop_command) + expect(Kernel). + to have_received(:system). + with(create_heroku_ext_schema_command) + expect(Kernel). + to have_received(:system). + with(specific_backup_id_restore_from_local_temp_backup_command(cores: 1)) + expect(Kernel). + to have_received(:system). + with(specific_backup_id_delete_local_temp_backup_command) + end + it "restores backups to development with Rubies that do not support Etc.nprocessors" do allow(IO).to receive(:read).and_return(database_fixture) allow(Kernel).to receive(:system) @@ -217,6 +248,20 @@ to have_received(:system).with(additional_argument_pass_through) end + it "restores from a specific backup ID when provided" do + stub_heroku_app_name + allow(Kernel).to receive(:system) + + Parity::Backup.new( + from: "production", + to: "staging", + backup_id: "b001", + ).restore + + expect(Kernel). + to have_received(:system).with(specific_backup_id_restore_command) + end + def stub_heroku_app_name heroku_app_name = instance_double(Parity::HerokuAppName, to_s: "parity-staging") @@ -239,7 +284,7 @@ def fixture_path(filename) end def drop_development_database_drop_command(db_name: default_db_name) - "dropdb --if-exists #{db_name} && createdb #{db_name}" + "dropdb --if-exists #{db_name} --force && createdb #{db_name}" end def create_heroku_ext_schema_command(db_name: default_db_name) @@ -258,11 +303,20 @@ def download_remote_database_command 'curl -o tmp/latest.backup "$(heroku pg:backups:url --remote production)"' end + def specific_backup_id_download_command + 'curl -o tmp/b001.backup "$(heroku pg:backups:url b001 --remote production)"' + end + def restore_from_local_temp_backup_command(cores: number_of_processes) "pg_restore tmp/latest.backup --verbose --no-acl --no-owner "\ "--dbname #{default_db_name} --jobs=#{cores} " end + def specific_backup_id_restore_from_local_temp_backup_command(cores: number_of_processes) + "pg_restore tmp/b001.backup --verbose --no-acl --no-owner "\ + "--dbname #{default_db_name} --jobs=#{cores} " + end + def number_of_processes 2 end @@ -271,6 +325,10 @@ def delete_local_temp_backup_command "rm tmp/latest.backup" end + def specific_backup_id_delete_local_temp_backup_command + "rm tmp/b001.backup" + end + def heroku_development_to_staging_passthrough(db_name: default_db_name) "heroku pg:push #{db_name} DATABASE_URL --remote staging " end @@ -290,6 +348,11 @@ def additional_argument_pass_through "--confirm thisismyapp-staging" end + def specific_backup_id_restore_command + "heroku pg:backups:restore `heroku pg:backups:url "\ + "b001 --remote production` DATABASE --remote staging " + end + def default_db_name "parity_development" end From 9614a282134c95ef9c86cc96d82b46991abd26e3 Mon Sep 17 00:00:00 2001 From: Seth Horsley Date: Thu, 5 Feb 2026 14:13:31 -0500 Subject: [PATCH 3/3] Refactor backup.rb: improve code style and add Rails environment loading - Replace 'alias' with 'alias_method' for better Ruby style - Standardize string continuation formatting across all Kernel.system calls - Improve method chaining readability in development_db method - Add Rails environment loading before reading database.yml to support credentials - Use consistent double quotes for string literals --- lib/parity/backup.rb | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/parity/backup.rb b/lib/parity/backup.rb index 3716000..289341c 100644 --- a/lib/parity/backup.rb +++ b/lib/parity/backup.rb @@ -28,9 +28,7 @@ def restore attr_reader :additional_args, :from, :to, :parallelize, :backup_id - alias :parallelize? :parallelize - - + alias_method :parallelize?, :parallelize def log_restore_info if backup_id @@ -45,8 +43,8 @@ def restore_from_development log_restore_info reset_remote_database Kernel.system( - "heroku pg:push #{development_db} DATABASE_URL --remote #{to} "\ - "#{additional_args}", + "heroku pg:push #{development_db} DATABASE_URL --remote #{to} " \ + "#{additional_args}" ) puts "Backup restoration to #{to} completed successfully!" end @@ -65,7 +63,7 @@ def restore_to_development def wipe_development_database Kernel.system( - "dropdb --if-exists #{development_db} --force && createdb #{development_db}", + "dropdb --if-exists #{development_db} --force && createdb #{development_db}" ) end @@ -79,8 +77,8 @@ def create_heroku_ext_schema def reset_remote_database Kernel.system( - "heroku pg:reset --remote #{to} #{additional_args} "\ - "--confirm #{heroku_app_name}", + "heroku pg:reset --remote #{to} #{additional_args} " \ + "--confirm #{heroku_app_name}" ) end @@ -96,12 +94,12 @@ def download_remote_backup if backup_id puts "Downloading backup #{backup_id} from #{from}..." Kernel.system( - "curl -o tmp/#{backup_id}.backup \"$(heroku pg:backups:url #{backup_id} --remote #{from})\"", + "curl -o tmp/#{backup_id}.backup \"$(heroku pg:backups:url #{backup_id} --remote #{from})\"" ) else puts "Downloading latest backup from #{from}..." Kernel.system( - "curl -o tmp/latest.backup \"$(heroku pg:backups:url --remote #{from})\"", + "curl -o tmp/latest.backup \"$(heroku pg:backups:url --remote #{from})\"" ) end end @@ -109,12 +107,12 @@ def download_remote_backup def restore_from_local_temp_backup puts "Restoring backup to #{development_db}..." # Filter out --backup-id from additional_args as it's not needed for pg_restore - filtered_args = additional_args.gsub(/--backup-id\s+\S+/, '').strip + filtered_args = additional_args.gsub(/--backup-id\s+\S+/, "").strip backup_filename = backup_id ? "#{backup_id}.backup" : "latest.backup" Kernel.system( - "pg_restore tmp/#{backup_filename} --verbose --no-acl --no-owner "\ - "--dbname #{development_db} --jobs=#{processor_cores} "\ - "#{filtered_args}", + "pg_restore tmp/#{backup_filename} --verbose --no-acl --no-owner " \ + "--dbname #{development_db} --jobs=#{processor_cores} " \ + "#{filtered_args}" ) end @@ -133,10 +131,10 @@ def restore_to_remote_environment log_restore_info reset_remote_database # Filter out --backup-id from additional_args as it's handled separately - filtered_args = additional_args.gsub(/--backup-id\s+\S+/, '').strip + filtered_args = additional_args.gsub(/--backup-id\s+\S+/, "").strip Kernel.system( - "heroku pg:backups:restore #{backup_from} --remote #{to} "\ - "#{filtered_args}", + "heroku pg:backups:restore #{backup_from} --remote #{to} " \ + "#{filtered_args}" ) puts "Backup restoration to #{to} completed successfully!" end @@ -154,12 +152,17 @@ def remote_db_backup_url end def development_db - YAML.safe_load(database_yaml_file, aliases: true). - fetch(DEVELOPMENT_ENVIRONMENT_KEY_NAME). - fetch(DATABASE_KEY_NAME) + YAML.safe_load(database_yaml_file, aliases: true) + .fetch(DEVELOPMENT_ENVIRONMENT_KEY_NAME) + .fetch(DATABASE_KEY_NAME) end def database_yaml_file + # Load Rails environment if Rails is not already loaded + # This is needed when database.yml uses Rails.application.credentials + unless defined?(Rails) + require File.expand_path("config/environment", Dir.pwd) + end ERB.new(IO.read(DATABASE_YML_RELATIVE_PATH)).result end