FIX: Create readonly functions during backup

Temporarily recreate already dropped functions in the discourse_functions schema in order to allow restoring of backups which still reference dropped functions.
This commit is contained in:
Gerhard Schlager 2019-08-08 16:06:27 +02:00
parent 8aa5df69f0
commit 7cb51d0e40
11 changed files with 164 additions and 43 deletions

View File

@ -4,24 +4,23 @@ require 'migration/column_dropper'
require 'badge_posts_view_manager' require 'badge_posts_view_manager'
class RemoveSuperfluousColumns < ActiveRecord::Migration[5.2] class RemoveSuperfluousColumns < ActiveRecord::Migration[5.2]
def up DROPPED_COLUMNS ||= {
{ user_profiles: %i{
user_profiles: %i{
card_image_badge_id card_image_badge_id
}, },
categories: %i{ categories: %i{
logo_url logo_url
background_url background_url
suppress_from_homepage suppress_from_homepage
}, },
groups: %i{ groups: %i{
visible visible
public public
alias_level alias_level
}, },
theme_fields: %i{target}, theme_fields: %i{target},
user_stats: %i{first_topic_unread_at}, user_stats: %i{first_topic_unread_at},
topics: %i{ topics: %i{
auto_close_at auto_close_at
auto_close_user_id auto_close_user_id
auto_close_started_at auto_close_started_at
@ -35,7 +34,7 @@ class RemoveSuperfluousColumns < ActiveRecord::Migration[5.2]
last_unread_at last_unread_at
vote_count vote_count
}, },
users: %i{ users: %i{
email email
email_always email_always
mailing_list_mode mailing_list_mode
@ -58,23 +57,27 @@ class RemoveSuperfluousColumns < ActiveRecord::Migration[5.2]
silenced silenced
trust_level_locked trust_level_locked
}, },
user_auth_tokens: %i{legacy}, user_auth_tokens: %i{legacy},
user_options: %i{theme_key}, user_options: %i{theme_key},
themes: %i{key}, themes: %i{key},
email_logs: %i{ email_logs: %i{
topic_id topic_id
reply_key reply_key
skipped skipped
skipped_reason skipped_reason
}, },
}.each do |table, columns| posts: %i{vote_count}
}
def up
BadgePostsViewManager.drop!
DROPPED_COLUMNS.each do |table, columns|
Migration::ColumnDropper.execute_drop(table, columns) Migration::ColumnDropper.execute_drop(table, columns)
end end
DB.exec "DROP FUNCTION IF EXISTS first_unread_topic_for(int)" DB.exec "DROP FUNCTION IF EXISTS first_unread_topic_for(int)"
BadgePostsViewManager.drop!
Migration::ColumnDropper.execute_drop(:posts, %i{vote_count})
BadgePostsViewManager.create! BadgePostsViewManager.create!
end end

View File

@ -3,12 +3,14 @@
require 'migration/table_dropper' require 'migration/table_dropper'
class RemoveSuperfluousTables < ActiveRecord::Migration[5.2] class RemoveSuperfluousTables < ActiveRecord::Migration[5.2]
def up DROPPED_TABLES ||= %i{
%i{
category_featured_users category_featured_users
versions versions
topic_status_updates topic_status_updates
}.each do |table| }
def up
DROPPED_TABLES.each do |table|
Migration::TableDropper.execute_drop(table) Migration::TableDropper.execute_drop(table)
end end
end end

View File

@ -3,8 +3,14 @@
require 'migration/column_dropper' require 'migration/column_dropper'
class DropGroupLockedTrustLevelFromUser < ActiveRecord::Migration[5.2] class DropGroupLockedTrustLevelFromUser < ActiveRecord::Migration[5.2]
DROPPED_COLUMNS ||= {
posts: %i{group_locked_trust_level}
}
def up def up
Migration::ColumnDropper.execute_drop(:posts, %i{group_locked_trust_level}) DROPPED_COLUMNS.each do |table, columns|
Migration::ColumnDropper.execute_drop(table, columns)
end
end end
def down def down

View File

@ -3,8 +3,14 @@
require 'migration/column_dropper' require 'migration/column_dropper'
class RemoveUploadedMetaIdFromCategory < ActiveRecord::Migration[5.2] class RemoveUploadedMetaIdFromCategory < ActiveRecord::Migration[5.2]
DROPPED_COLUMNS ||= {
categories: %i{uploaded_meta_id}
}
def up def up
Migration::ColumnDropper.execute_drop(:categories, %i{uploaded_meta_id}) DROPPED_COLUMNS.each do |table, columns|
Migration::ColumnDropper.execute_drop(table, columns)
end
end end
def down def down

View File

@ -3,11 +3,13 @@
require 'migration/table_dropper' require 'migration/table_dropper'
class DropUnusedAuthTablesAgain < ActiveRecord::Migration[5.2] class DropUnusedAuthTablesAgain < ActiveRecord::Migration[5.2]
def up DROPPED_TABLES ||= %i{
%i{
facebook_user_infos facebook_user_infos
twitter_user_infos twitter_user_infos
}.each do |table| }
def up
DROPPED_TABLES.each do |table|
Migration::TableDropper.execute_drop(table) Migration::TableDropper.execute_drop(table)
end end
end end

View File

@ -3,14 +3,16 @@
require 'migration/column_dropper' require 'migration/column_dropper'
class DropEmailUserOptionsColumns < ActiveRecord::Migration[5.2] class DropEmailUserOptionsColumns < ActiveRecord::Migration[5.2]
def up DROPPED_COLUMNS ||= {
{ user_options: %i{
user_options: %i{
email_direct email_direct
email_private_messages email_private_messages
email_always email_always
}, },
}.each do |table, columns| }
def up
DROPPED_COLUMNS.each do |table, columns|
Migration::ColumnDropper.execute_drop(table, columns) Migration::ColumnDropper.execute_drop(table, columns)
end end
end end

View File

@ -3,8 +3,14 @@
require 'migration/column_dropper' require 'migration/column_dropper'
class RemoveViaEmailFromInvite < ActiveRecord::Migration[5.2] class RemoveViaEmailFromInvite < ActiveRecord::Migration[5.2]
DROPPED_COLUMNS ||= {
invites: %i{via_email}
}
def up def up
Migration::ColumnDropper.execute_drop(:invites, %i{via_email}) DROPPED_COLUMNS.each do |table, columns|
Migration::ColumnDropper.execute_drop(table, columns)
end
end end
def down def down

View File

@ -63,6 +63,7 @@ module BackupRestore
validate_metadata validate_metadata
extract_dump extract_dump
create_missing_discourse_functions
if !can_restore_into_different_schema? if !can_restore_into_different_schema?
log "Cannot restore into different schema, restoring in-place" log "Cannot restore into different schema, restoring in-place"
@ -144,6 +145,7 @@ module BackupRestore
@logs = [] @logs = []
@readonly_mode_was_enabled = Discourse.readonly_mode? @readonly_mode_was_enabled = Discourse.readonly_mode?
@created_functions_for_table_columns = []
end end
def listen_for_shutdown_signal def listen_for_shutdown_signal
@ -561,8 +563,46 @@ module BackupRestore
log "Something went wrong while notifying user.", ex log "Something went wrong while notifying user.", ex
end end
def create_missing_discourse_functions
log "Creating missing functions in the discourse_functions schema"
all_readonly_table_columns = []
Dir[Rails.root.join(Discourse::DB_POST_MIGRATE_PATH, "*.rb")].each do |path|
require path
class_name = File.basename(path, ".rb").sub(/^\d+_/, "").camelize
migration_class = class_name.constantize
if migration_class.const_defined?(:DROPPED_TABLES)
migration_class::DROPPED_TABLES.each do |table_name|
all_readonly_table_columns << [table_name]
end
end
if migration_class.const_defined?(:DROPPED_COLUMNS)
migration_class::DROPPED_COLUMNS.each do |table_name, column_names|
column_names.each do |column_name|
all_readonly_table_columns << [table_name, column_name]
end
end
end
end
existing_function_names = Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" }
all_readonly_table_columns.each do |table_name, column_name|
function_name = Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false)
if !existing_function_names.include?(function_name)
Migration::BaseDropper.create_readonly_function(table_name, column_name)
@created_functions_for_table_columns << [table_name, column_name]
end
end
end
def clean_up def clean_up
log "Cleaning stuff up..." log "Cleaning stuff up..."
drop_created_discourse_functions
remove_tmp_directory remove_tmp_directory
unpause_sidekiq unpause_sidekiq
disable_readonly_mode if Discourse.readonly_mode? disable_readonly_mode if Discourse.readonly_mode?
@ -590,6 +630,15 @@ module BackupRestore
Stylesheet::Manager.cache.clear Stylesheet::Manager.cache.clear
end end
def drop_created_discourse_functions
log "Dropping function from the discourse_functions schema"
@created_functions_for_table_columns.each do |table_name, column_name|
Migration::BaseDropper.drop_readonly_function(table_name, column_name)
end
rescue => ex
log "Something went wrong while dropping functions from the discourse_functions schema", ex
end
def disable_readonly_mode def disable_readonly_mode
return if @readonly_mode_was_enabled return if @readonly_mode_was_enabled
log "Disabling readonly mode..." log "Disabling readonly mode..."

View File

@ -22,7 +22,11 @@ module Migration
SQL SQL
end end
def self.readonly_function_name(table_name, column_name = nil) def self.drop_readonly_function(table_name, column_name = nil)
DB.exec("DROP FUNCTION IF EXISTS #{readonly_function_name(table_name, column_name)} CASCADE")
end
def self.readonly_function_name(table_name, column_name = nil, with_schema: true)
function_name = [ function_name = [
"raise", "raise",
table_name, table_name,
@ -30,12 +34,7 @@ module Migration
"readonly()" "readonly()"
].compact.join("_") ].compact.join("_")
if DB.exec(<<~SQL).to_s == '1' if with_schema && function_schema_exists?
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = '#{FUNCTION_SCHEMA_NAME}'
SQL
"#{FUNCTION_SCHEMA_NAME}.#{function_name}" "#{FUNCTION_SCHEMA_NAME}.#{function_name}"
else else
function_name function_name
@ -51,5 +50,21 @@ module Migration
def self.readonly_trigger_name(table_name, column_name = nil) def self.readonly_trigger_name(table_name, column_name = nil)
[table_name, column_name, "readonly"].compact.join("_") [table_name, column_name, "readonly"].compact.join("_")
end end
def self.function_schema_exists?
DB.exec(<<~SQL).to_s == '1'
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = '#{FUNCTION_SCHEMA_NAME}'
SQL
end
def self.existing_discourse_function_names
DB.query_single(<<~SQL)
SELECT routine_name
FROM information_schema.routines
WHERE routine_type = 'FUNCTION' AND specific_schema = '#{FUNCTION_SCHEMA_NAME}'
SQL
end
end end
end end

View File

@ -28,13 +28,11 @@ module Migration
end end
end end
def self.drop_readonly(table, column) def self.drop_readonly(table_name, column_name)
DB.exec <<~SQL BaseDropper.drop_readonly_function(table_name, column_name)
DROP FUNCTION IF EXISTS #{BaseDropper.readonly_function_name(table, column)} CASCADE;
-- Backward compatibility for old functions created in the public # Backward compatibility for old functions created in the public schema
-- schema DB.exec("DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table_name, column_name)} CASCADE")
DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table, column)} CASCADE;
SQL
end end
end end
end end

View File

@ -32,4 +32,36 @@ describe 'Coding style' do
MSG MSG
end end
end end
describe 'Post Migrations' do
def check_offenses(files, method_name, constant_name)
method_name_regex = /#{Regexp.escape(method_name)}/
constant_name_regex = /#{Regexp.escape(constant_name)}/
offenses = files.reject { |file| is_valid?(file, method_name_regex, constant_name_regex) }
expect(offenses).to be_empty, <<~MSG
You need to use the constant #{constant_name} when you use
#{method_name} in order to help with restoring backups.
Please take a look at existing migrations to see how to use it correctly.
Offenses:
#{offenses.join("\n")}
MSG
end
def is_valid?(file, method_name_regex, constant_name_regex)
contains_method_name = File.open(file).grep(method_name_regex).any?
contains_constant_name = File.open(file).grep(constant_name_regex).any?
contains_method_name ? contains_constant_name : true
end
it 'ensures dropped tables and columns are stored in constants' do
migration_files = list_files('db/post_migrate', '**/*.rb')
check_offenses(migration_files, "ColumnDropper.execute_drop", "DROPPED_COLUMNS")
check_offenses(migration_files, "TableDropper.execute_drop", "DROPPED_TABLES")
end
end
end end