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:
parent
8aa5df69f0
commit
7cb51d0e40
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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..."
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue