summaryrefslogtreecommitdiff
path: root/lib/gitlab/database
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-28 18:26:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-28 18:26:46 +0000
commit5509e479900ee537980a126287c20327c41a61d6 (patch)
tree8272f06bd58b1518eca38975f95656ffc5497bd2 /lib/gitlab/database
parente0529f76a36026dc4bd51fbec1e5c52e7f3866e1 (diff)
downloadgitlab-ce-5509e479900ee537980a126287c20327c41a61d6.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/database')
-rw-r--r--lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb316
-rw-r--r--lib/gitlab/database/partitioning/list/convert_table.rb313
-rw-r--r--lib/gitlab/database/partitioning/list/locking_configuration.rb65
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb8
4 files changed, 382 insertions, 320 deletions
diff --git a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
deleted file mode 100644
index afca2368126..00000000000
--- a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
+++ /dev/null
@@ -1,316 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module Partitioning
- class ConvertTableToFirstListPartition
- UnableToPartition = Class.new(StandardError)
-
- SQL_STATEMENT_SEPARATOR = ";\n\n"
-
- PARTITIONING_CONSTRAINT_NAME = 'partitioning_constraint'
-
- attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value
-
- def initialize(
- migration_context:, table_name:, parent_table_name:, partitioning_column:,
- zero_partition_value:, lock_tables: [])
-
- @migration_context = migration_context
- @connection = migration_context.connection
- @table_name = table_name
- @parent_table_name = parent_table_name
- @partitioning_column = partitioning_column
- @zero_partition_value = zero_partition_value
- @lock_tables = Array.wrap(lock_tables)
- end
-
- def prepare_for_partitioning(async: false)
- assert_existing_constraints_partitionable
-
- add_partitioning_check_constraint(async: async)
- end
-
- def revert_preparation_for_partitioning
- migration_context.remove_check_constraint(table_name, partitioning_constraint.name)
- end
-
- def partition
- assert_existing_constraints_partitionable
- assert_partitioning_constraint_present
-
- create_parent_table
- attach_foreign_keys_to_parent
-
- lock_args = {
- raise_on_exhaustion: true,
- timing_configuration: lock_timing_configuration
- }
-
- migration_context.with_lock_retries(**lock_args) do
- redefine_loose_foreign_key_triggers do
- migration_context.execute(sql_to_convert_table)
- end
- end
- end
-
- def revert_partitioning
- migration_context.with_lock_retries(raise_on_exhaustion: true) do
- migration_context.execute(<<~SQL)
- ALTER TABLE #{connection.quote_table_name(parent_table_name)}
- DETACH PARTITION #{connection.quote_table_name(table_name)};
- SQL
-
- alter_sequences_sql = alter_sequence_statements(old_table: parent_table_name, new_table: table_name)
- .join(SQL_STATEMENT_SEPARATOR)
-
- migration_context.execute(alter_sequences_sql)
-
- # This takes locks for all the foreign keys that the parent table had.
- # However, those same locks were taken while detaching the partition, and we can't avoid that.
- # If we dropped the foreign key before detaching the partition to avoid this locking,
- # the drop would cascade to the child partitions and drop their foreign keys as well
- migration_context.drop_table(parent_table_name)
- end
-
- add_partitioning_check_constraint
- end
-
- private
-
- attr_reader :connection, :migration_context
-
- delegate :quote_table_name, :quote_column_name, to: :connection
-
- def sql_to_convert_table
- # The critical statement here is the attach_table_to_parent statement.
- # The following statements could be run in a later transaction,
- # but they acquire the same locks so it's much faster to incude them
- # here.
- [
- lock_tables_statement,
- attach_table_to_parent_statement,
- alter_sequence_statements(old_table: table_name, new_table: parent_table_name),
- remove_constraint_statement
- ].flatten.join(SQL_STATEMENT_SEPARATOR)
- end
-
- def table_identifier
- "#{connection.current_schema}.#{table_name}"
- end
-
- def assert_existing_constraints_partitionable
- violating_constraints = Gitlab::Database::PostgresConstraint
- .by_table_identifier(table_identifier)
- .primary_or_unique_constraints
- .not_including_column(partitioning_column)
- .to_a
-
- return if violating_constraints.empty?
-
- violation_messages = violating_constraints.map { |c| "#{c.name} on (#{c.column_names.join(', ')})" }
-
- raise UnableToPartition, <<~MSG
- Constraints on #{table_name} are incompatible with partitioning on #{partitioning_column}
-
- All primary key and unique constraints must include the partitioning column.
- Violations:
- #{violation_messages.join("\n")}
- MSG
- end
-
- def partitioning_constraint
- constraints_on_column = Gitlab::Database::PostgresConstraint
- .by_table_identifier(table_identifier)
- .check_constraints
- .including_column(partitioning_column)
-
- check_body = "CHECK ((#{partitioning_column} = #{zero_partition_value}))"
-
- constraints_on_column.find do |constraint|
- constraint.definition.start_with?(check_body)
- end
- end
-
- def assert_partitioning_constraint_present
- return if partitioning_constraint&.constraint_valid?
-
- raise UnableToPartition, <<~MSG
- Table #{table_name} is not ready for partitioning.
- Before partitioning, a check constraint must enforce that (#{partitioning_column} = #{zero_partition_value})
- MSG
- end
-
- def add_partitioning_check_constraint(async: false)
- return validate_partitioning_constraint_synchronously if partitioning_constraint.present?
-
- check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}"
- # Any constraint name would work. The constraint is found based on its definition before partitioning
- migration_context.add_check_constraint(
- table_name, check_body, PARTITIONING_CONSTRAINT_NAME,
- validate: !async
- )
-
- if async
- migration_context.prepare_async_check_constraint_validation(
- table_name, name: PARTITIONING_CONSTRAINT_NAME
- )
- end
-
- return if partitioning_constraint.present?
-
- raise UnableToPartition, <<~MSG
- Error adding partitioning constraint `#{PARTITIONING_CONSTRAINT_NAME}` for `#{table_name}`
- MSG
- end
-
- def validate_partitioning_constraint_synchronously
- if partitioning_constraint.constraint_valid?
- return Gitlab::AppLogger.info <<~MSG
- Nothing to do, the partitioning constraint exists and is valid for `#{table_name}`
- MSG
- end
-
- # Async validations are executed only on .com, we need to validate synchronously for self-managed
- migration_context.validate_check_constraint(table_name, partitioning_constraint.name)
- return if partitioning_constraint.constraint_valid?
-
- raise UnableToPartition, <<~MSG
- Error validating partitioning constraint `#{partitioning_constraint.name}` for `#{table_name}`
- MSG
- end
-
- def create_parent_table
- migration_context.execute(<<~SQL)
- CREATE TABLE IF NOT EXISTS #{quote_table_name(parent_table_name)} (
- LIKE #{quote_table_name(table_name)} INCLUDING ALL
- ) PARTITION BY LIST(#{quote_column_name(partitioning_column)})
- SQL
- end
-
- def attach_foreign_keys_to_parent
- migration_context.foreign_keys(table_name).each do |fk|
- # At this point no other connection knows about the parent table.
- # Thus the only contended lock in the following transaction is on fk.to_table.
- # So a deadlock is impossible.
-
- # If we're rerunning this migration after a failure to acquire a lock, the foreign key might already exist.
- # Don't try to recreate it in that case
- if migration_context.foreign_keys(parent_table_name)
- .any? { |p_fk| p_fk.options[:name] == fk.options[:name] }
- next
- end
-
- migration_context.with_lock_retries(raise_on_exhaustion: true) do
- migration_context.add_foreign_key(parent_table_name, fk.to_table, **fk.options)
- end
- end
- end
-
- def lock_tables_statement
- return if @lock_tables.empty?
-
- table_names = @lock_tables.map { |name| quote_table_name(name) }.join(', ')
-
- <<~SQL
- LOCK #{table_names} IN ACCESS EXCLUSIVE MODE
- SQL
- end
-
- def attach_table_to_parent_statement
- <<~SQL
- ALTER TABLE #{quote_table_name(parent_table_name)}
- ATTACH PARTITION #{table_name}
- FOR VALUES IN (#{zero_partition_value})
- SQL
- end
-
- def alter_sequence_statements(old_table:, new_table:)
- sequences_owned_by(old_table).map do |seq_info|
- seq_name, column_name = seq_info.values_at(:name, :column_name)
-
- statement_parts = []
-
- # If a different user owns the old table, the conversion process will fail to reassign the sequence
- # ownership to the new parent table (as it will be owned by the current user).
- # Force the old table to be owned by the current user in that case.
- unless current_user_owns_table?(old_table)
- statement_parts << set_current_user_owns_table_statement(old_table)
- end
-
- statement_parts << <<~SQL.chomp
- ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)}
- SQL
-
- statement_parts.join(SQL_STATEMENT_SEPARATOR)
- end
- end
-
- def remove_constraint_statement
- <<~SQL
- ALTER TABLE #{quote_table_name(parent_table_name)}
- DROP CONSTRAINT #{quote_table_name(partitioning_constraint.name)}
- SQL
- end
-
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/373887
- def sequences_owned_by(table_name)
- sequence_data = connection.exec_query(<<~SQL, nil, [table_name])
- SELECT seq_pg_class.relname AS seq_name,
- dep_pg_class.relname AS table_name,
- pg_attribute.attname AS col_name
- FROM pg_class seq_pg_class
- INNER JOIN pg_depend ON seq_pg_class.oid = pg_depend.objid
- INNER JOIN pg_class dep_pg_class ON pg_depend.refobjid = dep_pg_class.oid
- INNER JOIN pg_attribute ON dep_pg_class.oid = pg_attribute.attrelid
- AND pg_depend.refobjsubid = pg_attribute.attnum
- WHERE seq_pg_class.relkind = 'S'
- AND dep_pg_class.relname = $1
- SQL
-
- sequence_data.map do |seq_info|
- name, column_name = seq_info.values_at('seq_name', 'col_name')
- { name: name, column_name: column_name }
- end
- end
-
- def table_owner(table_name)
- connection.select_value(<<~SQL, nil, [table_name])
- SELECT tableowner FROM pg_tables WHERE tablename = $1
- SQL
- end
-
- def current_user_owns_table?(table_name)
- current_user = connection.select_value('select current_user')
- table_owner(table_name) == current_user
- end
-
- def set_current_user_owns_table_statement(table_name)
- <<~SQL.chomp
- ALTER TABLE #{connection.quote_table_name(table_name)} OWNER TO CURRENT_USER
- SQL
- end
-
- def lock_timing_configuration
- iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION
- aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] }
-
- iterations + aggressive_iterations
- end
-
- def redefine_loose_foreign_key_triggers
- if migration_context.has_loose_foreign_key?(table_name)
- migration_context.untrack_record_deletions(table_name)
-
- yield if block_given?
-
- migration_context.track_record_deletions(parent_table_name)
- migration_context.track_record_deletions(table_name)
- elsif block_given?
- yield
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/partitioning/list/convert_table.rb b/lib/gitlab/database/partitioning/list/convert_table.rb
new file mode 100644
index 00000000000..d40ddc7a4d8
--- /dev/null
+++ b/lib/gitlab/database/partitioning/list/convert_table.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Partitioning
+ module List
+ class ConvertTable
+ UnableToPartition = Class.new(StandardError)
+
+ SQL_STATEMENT_SEPARATOR = ";\n\n"
+
+ PARTITIONING_CONSTRAINT_NAME = 'partitioning_constraint'
+
+ attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value,
+ :locking_configuration
+
+ def initialize(
+ migration_context:, table_name:, parent_table_name:, partitioning_column:,
+ zero_partition_value:, lock_tables: [])
+
+ @migration_context = migration_context
+ @connection = migration_context.connection
+ @table_name = table_name
+ @parent_table_name = parent_table_name
+ @partitioning_column = partitioning_column
+ @zero_partition_value = zero_partition_value
+ @locking_configuration = LockingConfiguration.new(migration_context, table_locking_order: lock_tables)
+ end
+
+ def prepare_for_partitioning(async: false)
+ assert_existing_constraints_partitionable
+
+ add_partitioning_check_constraint(async: async)
+ end
+
+ def revert_preparation_for_partitioning
+ migration_context.remove_check_constraint(table_name, partitioning_constraint.name)
+ end
+
+ def partition
+ assert_existing_constraints_partitionable
+ assert_partitioning_constraint_present
+
+ create_parent_table
+ attach_foreign_keys_to_parent
+
+ locking_configuration.with_lock_retries do
+ redefine_loose_foreign_key_triggers do
+ migration_context.execute(sql_to_convert_table)
+ end
+ end
+ end
+
+ def revert_partitioning
+ migration_context.with_lock_retries(raise_on_exhaustion: true) do
+ migration_context.execute(<<~SQL)
+ ALTER TABLE #{connection.quote_table_name(parent_table_name)}
+ DETACH PARTITION #{connection.quote_table_name(table_name)};
+ SQL
+
+ alter_sequences_sql = alter_sequence_statements(old_table: parent_table_name, new_table: table_name)
+ .join(SQL_STATEMENT_SEPARATOR)
+
+ migration_context.execute(alter_sequences_sql)
+
+ # This takes locks for all the foreign keys that the parent table had.
+ # However, those same locks were taken while detaching the partition, and we can't avoid that.
+ # If we dropped the foreign key before detaching the partition to avoid this locking,
+ # the drop would cascade to the child partitions and drop their foreign keys as well
+ migration_context.drop_table(parent_table_name)
+ end
+
+ add_partitioning_check_constraint
+ end
+
+ private
+
+ attr_reader :connection, :migration_context
+
+ delegate :quote_table_name, :quote_column_name, :current_schema, to: :connection
+
+ def sql_to_convert_table
+ # The critical statement here is the attach_table_to_parent statement.
+ # The following statements could be run in a later transaction,
+ # but they acquire the same locks so it's much faster to include them
+ # here.
+ [
+ locking_configuration.locking_statement_for(tables_that_will_lock_during_partitioning),
+ attach_table_to_parent_statement,
+ alter_sequence_statements(old_table: table_name, new_table: parent_table_name),
+ remove_constraint_statement
+ ].flatten.join(SQL_STATEMENT_SEPARATOR)
+ end
+
+ def table_identifier
+ "#{current_schema}.#{table_name}"
+ end
+
+ def assert_existing_constraints_partitionable
+ violating_constraints = Gitlab::Database::PostgresConstraint
+ .by_table_identifier(table_identifier)
+ .primary_or_unique_constraints
+ .not_including_column(partitioning_column)
+ .to_a
+
+ return if violating_constraints.empty?
+
+ violation_messages = violating_constraints.map { |c| "#{c.name} on (#{c.column_names.join(', ')})" }
+
+ raise UnableToPartition, <<~MSG
+ Constraints on #{table_name} are incompatible with partitioning on #{partitioning_column}
+
+ All primary key and unique constraints must include the partitioning column.
+ Violations:
+ #{violation_messages.join("\n")}
+ MSG
+ end
+
+ def partitioning_constraint
+ constraints_on_column = Gitlab::Database::PostgresConstraint
+ .by_table_identifier(table_identifier)
+ .check_constraints
+ .including_column(partitioning_column)
+
+ check_body = "CHECK ((#{partitioning_column} = #{zero_partition_value}))"
+
+ constraints_on_column.find do |constraint|
+ constraint.definition.start_with?(check_body)
+ end
+ end
+
+ def assert_partitioning_constraint_present
+ return if partitioning_constraint&.constraint_valid?
+
+ raise UnableToPartition, <<~MSG
+ Table #{table_name} is not ready for partitioning.
+ Before partitioning, a check constraint must enforce that (#{partitioning_column} = #{zero_partition_value})
+ MSG
+ end
+
+ def add_partitioning_check_constraint(async: false)
+ return validate_partitioning_constraint_synchronously if partitioning_constraint.present?
+
+ check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}"
+ # Any constraint name would work. The constraint is found based on its definition before partitioning
+ migration_context.add_check_constraint(
+ table_name, check_body, PARTITIONING_CONSTRAINT_NAME,
+ validate: !async
+ )
+
+ if async
+ migration_context.prepare_async_check_constraint_validation(
+ table_name, name: PARTITIONING_CONSTRAINT_NAME
+ )
+ end
+
+ return if partitioning_constraint.present?
+
+ raise UnableToPartition, <<~MSG
+ Error adding partitioning constraint `#{PARTITIONING_CONSTRAINT_NAME}` for `#{table_name}`
+ MSG
+ end
+
+ def validate_partitioning_constraint_synchronously
+ if partitioning_constraint.constraint_valid?
+ return Gitlab::AppLogger.info <<~MSG
+ Nothing to do, the partitioning constraint exists and is valid for `#{table_name}`
+ MSG
+ end
+
+ # Async validations are executed only on .com, we need to validate synchronously for self-managed
+ migration_context.validate_check_constraint(table_name, partitioning_constraint.name)
+ return if partitioning_constraint.constraint_valid?
+
+ raise UnableToPartition, <<~MSG
+ Error validating partitioning constraint `#{partitioning_constraint.name}` for `#{table_name}`
+ MSG
+ end
+
+ def create_parent_table
+ migration_context.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS #{quote_table_name(parent_table_name)} (
+ LIKE #{quote_table_name(table_name)} INCLUDING ALL
+ ) PARTITION BY LIST(#{quote_column_name(partitioning_column)})
+ SQL
+ end
+
+ def attach_foreign_keys_to_parent
+ migration_context.foreign_keys(table_name).each do |fk|
+ # At this point no other connection knows about the parent table.
+ # Thus the only contended lock in the following transaction is on fk.to_table.
+ # So a deadlock is impossible.
+
+ # If we're rerunning this migration after a failure to acquire a lock, the foreign key might already exist
+ # Don't try to recreate it in that case
+ if migration_context.foreign_keys(parent_table_name)
+ .any? { |p_fk| p_fk.options[:name] == fk.options[:name] }
+ next
+ end
+
+ migration_context.with_lock_retries(raise_on_exhaustion: true) do
+ migration_context.add_foreign_key(parent_table_name, fk.to_table, **fk.options)
+ end
+ end
+ end
+
+ def attach_table_to_parent_statement
+ <<~SQL
+ ALTER TABLE #{quote_table_name(parent_table_name)}
+ ATTACH PARTITION #{table_name}
+ FOR VALUES IN (#{zero_partition_value})
+ SQL
+ end
+
+ def alter_sequence_statements(old_table:, new_table:)
+ sequences_owned_by(old_table).map do |seq_info|
+ seq_name, column_name = seq_info.values_at(:name, :column_name)
+
+ statement_parts = []
+
+ # If a different user owns the old table, the conversion process will fail to reassign the sequence
+ # ownership to the new parent table (as it will be owned by the current user).
+ # Force the old table to be owned by the current user in that case.
+ unless current_user_owns_table?(old_table)
+ statement_parts << set_current_user_owns_table_statement(old_table)
+ end
+
+ statement_parts << <<~SQL.chomp
+ ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)}
+ SQL
+
+ statement_parts.join(SQL_STATEMENT_SEPARATOR)
+ end
+ end
+
+ def remove_constraint_statement
+ <<~SQL
+ ALTER TABLE #{quote_table_name(parent_table_name)}
+ DROP CONSTRAINT #{quote_table_name(partitioning_constraint.name)}
+ SQL
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/373887
+ def sequences_owned_by(table_name)
+ sequence_data = connection.exec_query(<<~SQL, nil, [table_name])
+ SELECT seq_pg_class.relname AS seq_name,
+ dep_pg_class.relname AS table_name,
+ pg_attribute.attname AS col_name
+ FROM pg_class seq_pg_class
+ INNER JOIN pg_depend ON seq_pg_class.oid = pg_depend.objid
+ INNER JOIN pg_class dep_pg_class ON pg_depend.refobjid = dep_pg_class.oid
+ INNER JOIN pg_attribute ON dep_pg_class.oid = pg_attribute.attrelid
+ AND pg_depend.refobjsubid = pg_attribute.attnum
+ WHERE seq_pg_class.relkind = 'S'
+ AND dep_pg_class.relname = $1
+ SQL
+
+ sequence_data.map do |seq_info|
+ name, column_name = seq_info.values_at('seq_name', 'col_name')
+ { name: name, column_name: column_name }
+ end
+ end
+
+ def table_owner(table_name)
+ connection.select_value(<<~SQL, nil, [table_name])
+ SELECT tableowner FROM pg_tables WHERE tablename = $1
+ SQL
+ end
+
+ def current_user_owns_table?(table_name)
+ current_user = connection.select_value('select current_user')
+ table_owner(table_name) == current_user
+ end
+
+ def set_current_user_owns_table_statement(table_name)
+ <<~SQL.chomp
+ ALTER TABLE #{connection.quote_table_name(table_name)} OWNER TO CURRENT_USER
+ SQL
+ end
+
+ def table_name_for_identifier(table_identifier)
+ /^\w+\.(\w+)*$/.match(table_identifier)[1]
+ end
+
+ def redefine_loose_foreign_key_triggers
+ if migration_context.has_loose_foreign_key?(table_name)
+ migration_context.untrack_record_deletions(table_name)
+
+ yield if block_given?
+
+ migration_context.track_record_deletions(parent_table_name)
+ migration_context.track_record_deletions(table_name)
+ elsif block_given?
+ yield
+ end
+ end
+
+ def tables_that_will_lock_during_partitioning
+ # Locks are taken against the table + all tables that reference it by foreign key
+ # postgres_foreign_keys.referenced_table_name gives the table name that we need here directly, but that
+ # column did not exist yet during the migration 20221021145820_create_routing_table_for_builds_metadata_v2
+ # To ensure compatibility with that migration if it is run with this code, use referenced_table_identifier
+ # here.
+ referenced_tables = Gitlab::Database::PostgresForeignKey
+ .by_constrained_table_identifier(table_identifier)
+ .map { |fk| table_name_for_identifier(fk.referenced_table_identifier) }
+ referenced_tables + [table_name]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning/list/locking_configuration.rb b/lib/gitlab/database/partitioning/list/locking_configuration.rb
new file mode 100644
index 00000000000..02d20383de4
--- /dev/null
+++ b/lib/gitlab/database/partitioning/list/locking_configuration.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Partitioning
+ module List
+ class LockingConfiguration
+ attr_reader :migration_context
+
+ def initialize(migration_context, table_locking_order:)
+ @migration_context = migration_context
+ @table_locking_order = table_locking_order.map(&:to_s)
+ assert_table_names_unqualified!(@table_locking_order)
+ end
+
+ def locking_statement_for(tables)
+ tables_to_lock = locking_order_for(tables)
+
+ return if tables_to_lock.empty?
+
+ table_names = tables_to_lock.map { |name| migration_context.quote_table_name(name) }.join(', ')
+
+ <<~SQL
+ LOCK #{table_names} IN ACCESS EXCLUSIVE MODE
+ SQL
+ end
+
+ # Sorts and subsets `tables` to the tables that were explicitly requested for locking
+ # in the order that that locking was requested.
+ def locking_order_for(tables)
+ tables = Array.wrap(tables)
+ assert_table_names_unqualified!(tables)
+
+ @table_locking_order.intersection(tables.map(&:to_s))
+ end
+
+ def lock_timing_configuration
+ iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION
+ aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] }
+
+ iterations + aggressive_iterations
+ end
+
+ def with_lock_retries(&block)
+ lock_args = {
+ raise_on_exhaustion: true,
+ timing_configuration: lock_timing_configuration
+ }
+
+ migration_context.with_lock_retries(**lock_args, &block)
+ end
+
+ private
+
+ def assert_table_names_unqualified!(table_names)
+ tables = Array.wrap(table_names).select { |name| name.to_s.include?('.') }
+ return if tables.empty?
+
+ raise ArgumentError, "All table names must be unqualified, but #{tables.join(', ')} include schema"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 8b49cb00bdf..61e95dbe1a4 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -258,7 +258,7 @@ module Gitlab
def prepare_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:, async: false)
validate_not_in_transaction!(:prepare_constraint_for_list_partitioning)
- Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ Gitlab::Database::Partitioning::List::ConvertTable
.new(migration_context: self,
table_name: table_name,
parent_table_name: parent_table_name,
@@ -270,7 +270,7 @@ module Gitlab
def revert_preparing_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
validate_not_in_transaction!(:revert_preparing_constraint_for_list_partitioning)
- Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ Gitlab::Database::Partitioning::List::ConvertTable
.new(migration_context: self,
table_name: table_name,
parent_table_name: parent_table_name,
@@ -282,7 +282,7 @@ module Gitlab
def convert_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:, lock_tables: [])
validate_not_in_transaction!(:convert_table_to_first_list_partition)
- Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ Gitlab::Database::Partitioning::List::ConvertTable
.new(migration_context: self,
table_name: table_name,
parent_table_name: parent_table_name,
@@ -295,7 +295,7 @@ module Gitlab
def revert_converting_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
validate_not_in_transaction!(:revert_converting_table_to_first_list_partition)
- Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ Gitlab::Database::Partitioning::List::ConvertTable
.new(migration_context: self,
table_name: table_name,
parent_table_name: parent_table_name,