diff --git a/changelogs/unreleased/ab-partition-management.yml b/changelogs/unreleased/ab-partition-management.yml new file mode 100644 index 0000000000000000000000000000000000000000..35d9ce9ed0ad2fc4f5504dfcc0b39a39a2f2a058 --- /dev/null +++ b/changelogs/unreleased/ab-partition-management.yml @@ -0,0 +1,5 @@ +--- +title: Create time-space partitions in separate schema +merge_request: 34504 +author: +type: other diff --git a/config/initializers/active_record_schema_ignore_tables.rb b/config/initializers/active_record_schema_ignore_tables.rb index 661135f8ade36a26861cd244efc18e5f624dabd3..559455cbf46720753c448d00f50a21a8a561124a 100644 --- a/config/initializers/active_record_schema_ignore_tables.rb +++ b/config/initializers/active_record_schema_ignore_tables.rb @@ -1,2 +1,5 @@ # Ignore table used temporarily in background migration ActiveRecord::SchemaDumper.ignore_tables = ["untracked_files_for_uploads"] + +# Ignore dynamically managed partitions in static application schema +ActiveRecord::SchemaDumper.ignore_tables += ["partitions_dynamic.*"] diff --git a/db/migrate/20200615101135_create_dynamic_partitions_schema.rb b/db/migrate/20200615101135_create_dynamic_partitions_schema.rb new file mode 100644 index 0000000000000000000000000000000000000000..4e6636a216460d7bee607dbb0e434b82fd379845 --- /dev/null +++ b/db/migrate/20200615101135_create_dynamic_partitions_schema.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateDynamicPartitionsSchema < ActiveRecord::Migration[6.0] + include Gitlab::Database::SchemaHelpers + + DOWNTIME = false + + def up + execute 'CREATE SCHEMA partitions_dynamic' + + create_comment(:schema, :partitions_dynamic, <<~EOS.strip) + Schema to hold partitions managed dynamically from the application, e.g. for time space partitioning. + EOS + end + + def down + execute 'DROP SCHEMA partitions_dynamic' + end +end diff --git a/db/structure.sql b/db/structure.sql index 7cd30c15c332aae2d95344f5b1c87e92fd99134a..0f2ff89c7036685204ca1bebf0a43bfa20f6b764 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,5 +1,9 @@ SET search_path=public; +CREATE SCHEMA partitions_dynamic; + +COMMENT ON SCHEMA partitions_dynamic IS 'Schema to hold partitions managed dynamically from the application, e.g. for time space partitioning.'; + CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; CREATE TABLE public.abuse_reports ( @@ -13995,6 +13999,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200609142508 20200609212701 20200615083635 +20200615101135 20200615121217 20200615123055 20200615232735 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 f77fbe98df16747c3b99b79c6452075e575d6e38..d57ad03bd0507d2b69de346c886b0b7f1823cb6e 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -8,6 +8,7 @@ module TableManagementHelpers WHITELISTED_TABLES = %w[audit_events].freeze ERROR_SCOPE = 'table partitioning' + DYNAMIC_PARTITIONS_SCHEMA = 'partitions_dynamic' # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column. # One partition is created per month between the given `min_date` and `max_date`. @@ -125,7 +126,7 @@ def create_daterange_partitions(table_name, column_name, min_date, max_date) min_date = min_date.beginning_of_month.to_date max_date = max_date.next_month.beginning_of_month.to_date - create_range_partition_safely("#{table_name}_000000", table_name, 'MINVALUE', to_sql_date_literal(min_date)) + create_range_partition_safely("#{table_name}_000000", table_name, 'MINVALUE', to_sql_date_literal(min_date), schema: DYNAMIC_PARTITIONS_SCHEMA) while min_date < max_date partition_name = "#{table_name}_#{min_date.strftime('%Y%m')}" @@ -133,7 +134,7 @@ def create_daterange_partitions(table_name, column_name, min_date, max_date) lower_bound = to_sql_date_literal(min_date) upper_bound = to_sql_date_literal(next_date) - create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound) + create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound, schema: DYNAMIC_PARTITIONS_SCHEMA) min_date = next_date end end @@ -142,8 +143,8 @@ def to_sql_date_literal(date) connection.quote(date.strftime('%Y-%m-%d')) end - def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound) - if table_exists?(partition_name) + def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound, schema:) + if table_exists?("#{schema}.#{partition_name}") # rubocop:disable Gitlab/RailsLogger Rails.logger.warn "Partition not created because it already exists" \ " (this may be due to an aborted migration or similar): partition_name: #{partition_name}" @@ -151,7 +152,7 @@ def create_range_partition_safely(partition_name, table_name, lower_bound, upper return end - create_range_partition(partition_name, table_name, lower_bound, upper_bound) + create_range_partition(partition_name, table_name, lower_bound, upper_bound, schema: schema) end def create_sync_trigger(source_table, target_table, unique_key) diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb index 8e544307d81934b844e50d5c209a6e90678ca757..fee52024a97aacded3008feb0f9a6113a41f8350 100644 --- a/lib/gitlab/database/schema_helpers.rb +++ b/lib/gitlab/database/schema_helpers.rb @@ -69,9 +69,11 @@ def assert_not_in_transaction_block(scope:) private - def create_range_partition(partition_name, table_name, lower_bound, upper_bound) + def create_range_partition(partition_name, table_name, lower_bound, upper_bound, schema:) + raise ArgumentError, 'explicit schema is required but currently missing' unless schema + execute(<<~SQL) - CREATE TABLE #{partition_name} PARTITION OF #{table_name} + CREATE TABLE #{schema}.#{partition_name} PARTITION OF #{table_name} FOR VALUES FROM (#{lower_bound}) TO (#{upper_bound}) SQL end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 586b57d2002198160652b7fb40573713c12f407f..60fd039da332d3934b3d35076875f854105b92a6 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -241,7 +241,7 @@ describe '#drop_partitioned_table_for' do let(:expected_tables) do - %w[000000 201912 202001 202002].map { |suffix| "#{partitioned_table}_#{suffix}" }.unshift(partitioned_table) + %w[000000 201912 202001 202002].map { |suffix| "partitions_dynamic.#{partitioned_table}_#{suffix}" }.unshift(partitioned_table) end context 'when the table is not whitelisted' do diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb index 98a13915d7698ec39f0ce222e9e56c524f90e955..7aedc6953aa214eba2bacb75a2c8896ffd245a46 100644 --- a/spec/support/helpers/partitioning_helpers.rb +++ b/spec/support/helpers/partitioning_helpers.rb @@ -8,8 +8,8 @@ def expect_table_partitioned_by(table, columns, part_type: :range) expect(columns_with_part_type).to match_array(actual_columns) end - def expect_range_partition_of(partition_name, table_name, min_value, max_value) - definition = find_partition_definition(partition_name) + def expect_range_partition_of(partition_name, table_name, min_value, max_value, schema: 'partitions_dynamic') + definition = find_partition_definition(partition_name, schema: schema) expect(definition).not_to be_nil expect(definition['base_table']).to eq(table_name.to_s) @@ -40,7 +40,7 @@ def find_partitioned_columns(table) SQL end - def find_partition_definition(partition) + def find_partition_definition(partition, schema: 'partitions_dynamic') connection.select_one(<<~SQL) select parent_class.relname as base_table, @@ -48,7 +48,10 @@ def find_partition_definition(partition) from pg_class inner join pg_inherits i on pg_class.oid = inhrelid inner join pg_class parent_class on parent_class.oid = inhparent - where pg_class.relname = '#{partition}' and pg_class.relispartition; + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and pg_class.relname = '#{partition}' + and pg_class.relispartition SQL end end