Commit 7850f3a4 authored by Yannis Roussos's avatar Yannis Roussos

Merge branch '241267-rename-partitions-when-swapping-tables' into 'master'

Rename partitions when swapping partitioned tables

See merge request gitlab-org/gitlab!46489
parents 9255c8b8 5d1a3f59
...@@ -43,38 +43,70 @@ module Gitlab ...@@ -43,38 +43,70 @@ module Gitlab
end end
def sql_to_replace_table def sql_to_replace_table
@sql_to_replace_table ||= [ @sql_to_replace_table ||= combined_sql_statements.map(&:chomp).join(DELIMITER)
drop_default_sql(original_table, primary_key_column), end
set_default_sql(replacement_table, primary_key_column, "nextval('#{quote_table_name(sequence)}'::regclass)"),
def combined_sql_statements
statements = []
statements << alter_column_default(original_table, primary_key_column, expression: nil)
statements << alter_column_default(replacement_table, primary_key_column,
expression: "nextval('#{quote_table_name(sequence)}'::regclass)")
statements << alter_sequence_owned_by(sequence, replacement_table, primary_key_column)
change_sequence_owner_sql(sequence, replacement_table, primary_key_column), rename_table_objects(statements, original_table, replaced_table, original_primary_key, replaced_primary_key)
rename_table_objects(statements, replacement_table, original_table, replacement_primary_key, original_primary_key)
rename_table_sql(original_table, replaced_table), statements
rename_constraint_sql(replaced_table, original_primary_key, replaced_primary_key), end
def rename_table_objects(statements, old_table, new_table, old_primary_key, new_primary_key)
statements << rename_table(old_table, new_table)
statements << rename_constraint(new_table, old_primary_key, new_primary_key)
rename_table_sql(replacement_table, original_table), rename_partitions(statements, old_table, new_table)
rename_constraint_sql(original_table, replacement_primary_key, original_primary_key)
].join(DELIMITER)
end end
def drop_default_sql(table, column) def rename_partitions(statements, old_table_name, new_table_name)
"ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} DROP DEFAULT" Gitlab::Database::PostgresPartition.for_parent_table(old_table_name).each do |partition|
new_partition_name = partition.name.sub(/#{old_table_name}/, new_table_name)
old_primary_key = default_primary_key(partition.name)
new_primary_key = default_primary_key(new_partition_name)
statements << rename_constraint(partition.identifier, old_primary_key, new_primary_key)
statements << rename_table(partition.identifier, new_partition_name)
end
end end
def set_default_sql(table, column, expression) def alter_column_default(table_name, column_name, expression:)
"ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} SET DEFAULT #{expression}" default_clause = expression.nil? ? 'DROP DEFAULT' : "SET DEFAULT #{expression}"
<<~SQL
ALTER TABLE #{quote_table_name(table_name)}
ALTER COLUMN #{quote_column_name(column_name)} #{default_clause}
SQL
end end
def change_sequence_owner_sql(sequence, table, column) def alter_sequence_owned_by(sequence_name, table_name, column_name)
"ALTER SEQUENCE #{quote_table_name(sequence)} OWNED BY #{quote_table_name(table)}.#{quote_column_name(column)}" <<~SQL
ALTER SEQUENCE #{quote_table_name(sequence_name)}
OWNED BY #{quote_table_name(table_name)}.#{quote_column_name(column_name)}
SQL
end end
def rename_table_sql(old_name, new_name) def rename_table(old_name, new_name)
"ALTER TABLE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}" <<~SQL
ALTER TABLE #{quote_table_name(old_name)}
RENAME TO #{quote_table_name(new_name)}
SQL
end end
def rename_constraint_sql(table, old_name, new_name) def rename_constraint(table_name, old_name, new_name)
"ALTER TABLE #{quote_table_name(table)} RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}" <<~SQL
ALTER TABLE #{quote_table_name(table_name)}
RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
SQL
end end
end end
end end
......
...@@ -179,7 +179,8 @@ module Gitlab ...@@ -179,7 +179,8 @@ module Gitlab
# Replaces a non-partitioned table with its partitioned copy. This is the final step in a partitioning # Replaces a non-partitioned table with its partitioned copy. This is the final step in a partitioning
# migration, which makes the partitioned table ready for use by the application. The partitioned copy should be # migration, which makes the partitioned table ready for use by the application. The partitioned copy should be
# replaced with the original table in such a way that it appears seamless to any database clients. The replaced # replaced with the original table in such a way that it appears seamless to any database clients. The replaced
# table will be renamed to "#{replaced_table}_archived" # table will be renamed to "#{replaced_table}_archived". Partitions and primary key constraints will also be
# renamed to match the naming scheme of the parent table.
# #
# **NOTE** This method should only be used after all other migration steps have completed successfully. # **NOTE** This method should only be used after all other migration steps have completed successfully.
# There are several limitations to this method that MUST be handled before, or during, the swap migration: # There are several limitations to this method that MUST be handled before, or during, the swap migration:
...@@ -415,7 +416,7 @@ module Gitlab ...@@ -415,7 +416,7 @@ module Gitlab
end end
def replace_table(original_table_name, replacement_table_name, replaced_table_name, primary_key_name) def replace_table(original_table_name, replacement_table_name, replaced_table_name, primary_key_name)
replace_table = Gitlab::Database::Partitioning::ReplaceTable.new(original_table_name, replace_table = Gitlab::Database::Partitioning::ReplaceTable.new(original_table_name.to_s,
replacement_table_name, replaced_table_name, primary_key_name) replacement_table_name, replaced_table_name, primary_key_name)
with_lock_retries do with_lock_retries do
......
...@@ -30,9 +30,6 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do ...@@ -30,9 +30,6 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do
created_at timestamptz NOT NULL, created_at timestamptz NOT NULL,
PRIMARY KEY (id, created_at)) PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at); PARTITION BY RANGE (created_at);
CREATE TABLE #{replacement_table}_202001 PARTITION OF #{replacement_table}
FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
SQL SQL
end end
...@@ -56,13 +53,58 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do ...@@ -56,13 +53,58 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do
end end
it 'renames the primary key constraints to match the new table names' do it 'renames the primary key constraints to match the new table names' do
expect(primary_key_constraint_name(original_table)).to eq(original_primary_key) expect_primary_keys_after_tables([original_table, replacement_table])
expect(primary_key_constraint_name(replacement_table)).to eq(replacement_primary_key)
expect_table_to_be_replaced { replace_table } expect_table_to_be_replaced { replace_table }
expect(primary_key_constraint_name(original_table)).to eq(original_primary_key) expect_primary_keys_after_tables([original_table, archived_table])
expect(primary_key_constraint_name(archived_table)).to eq(archived_primary_key) end
context 'when the table has partitions' do
before do
connection.execute(<<~SQL)
CREATE TABLE gitlab_partitions_dynamic.#{replacement_table}_202001 PARTITION OF #{replacement_table}
FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
CREATE TABLE gitlab_partitions_dynamic.#{replacement_table}_202002 PARTITION OF #{replacement_table}
FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
SQL
end
it 'renames the partitions to match the new table name' do
expect(partitions_for_parent_table(original_table).count).to eq(0)
expect(partitions_for_parent_table(replacement_table).count).to eq(2)
expect_table_to_be_replaced { replace_table }
expect(partitions_for_parent_table(archived_table).count).to eq(0)
partitions = partitions_for_parent_table(original_table).all
expect(partitions.size).to eq(2)
expect(partitions[0]).to have_attributes(
identifier: "gitlab_partitions_dynamic.#{original_table}_202001",
condition: "FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')")
expect(partitions[1]).to have_attributes(
identifier: "gitlab_partitions_dynamic.#{original_table}_202002",
condition: "FOR VALUES FROM ('2020-02-01 00:00:00+00') TO ('2020-03-01 00:00:00+00')")
end
it 'renames the primary key constraints to match the new partition names' do
original_partitions = ["#{replacement_table}_202001", "#{replacement_table}_202002"]
expect_primary_keys_after_tables(original_partitions, schema: 'gitlab_partitions_dynamic')
expect_table_to_be_replaced { replace_table }
renamed_partitions = ["#{original_table}_202001", "#{original_table}_202002"]
expect_primary_keys_after_tables(renamed_partitions, schema: 'gitlab_partitions_dynamic')
end
end
def partitions_for_parent_table(table)
Gitlab::Database::PostgresPartition.for_parent_table(table)
end end
def expect_table_to_be_replaced(&block) def expect_table_to_be_replaced(&block)
......
...@@ -24,6 +24,14 @@ module TableSchemaHelpers ...@@ -24,6 +24,14 @@ module TableSchemaHelpers
expect(index_exists_by_name(name, schema: schema)).to be_nil expect(index_exists_by_name(name, schema: schema)).to be_nil
end end
def expect_primary_keys_after_tables(tables, schema: nil)
tables.each do |table|
primary_key = primary_key_constraint_name(table, schema: schema)
expect(primary_key).to eq("#{table}_pkey")
end
end
def table_oid(name) def table_oid(name)
connection.select_value(<<~SQL) connection.select_value(<<~SQL)
SELECT oid SELECT oid
...@@ -75,13 +83,15 @@ module TableSchemaHelpers ...@@ -75,13 +83,15 @@ module TableSchemaHelpers
SQL SQL
end end
def primary_key_constraint_name(table_name) def primary_key_constraint_name(table_name, schema: nil)
table_name = schema ? "#{schema}.#{table_name}" : table_name
connection.select_value(<<~SQL) connection.select_value(<<~SQL)
SELECT SELECT
conname AS constraint_name conname AS constraint_name
FROM pg_catalog.pg_constraint FROM pg_catalog.pg_constraint
WHERE conrelid = '#{table_name}'::regclass WHERE pg_constraint.conrelid = '#{table_name}'::regclass
AND contype = 'p' AND pg_constraint.contype = 'p'
SQL SQL
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment