diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /spec/lib/gitlab/database | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/lib/gitlab/database')
9 files changed, 577 insertions, 37 deletions
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index 31a8b4afa03..a1cc759e011 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -141,6 +141,29 @@ RSpec.describe Gitlab::Database::BatchCount do described_class.batch_count(model) end + it 'does not use BETWEEN to define the range' do + batch_size = Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE + 1 + issue = nil + + travel_to(Date.tomorrow) do + issue = create(:issue) # created_at: 00:00:00 + create(:issue, created_at: issue.created_at + batch_size - 0.5) # created_at: 00:20:50.5 + create(:issue, created_at: issue.created_at + batch_size) # created_at: 00:20:51 + end + + # When using BETWEEN, the range condition looks like: + # Batch 1: WHERE "issues"."created_at" BETWEEN "2020-10-09 00:00:00" AND "2020-10-09 00:20:50" + # Batch 2: WHERE "issues"."created_at" BETWEEN "2020-10-09 00:20:51" AND "2020-10-09 00:41:41" + # We miss the issue created at 00:20:50.5 because we prevent the batches from overlapping (start..(finish - 1)) + # See https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_BETWEEN_.28especially_with_timestamps.29 + + # When using >= AND <, we eliminate any gaps between batches (start...finish) + # This is useful when iterating over a timestamp column + # Batch 1: WHERE "issues"."created_at" >= "2020-10-09 00:00:00" AND "issues"."created_at" < "2020-10-09 00:20:51" + # Batch 1: WHERE "issues"."created_at" >= "2020-10-09 00:20:51" AND "issues"."created_at" < "2020-10-09 00:41:42" + expect(described_class.batch_count(model, :created_at, batch_size: batch_size, start: issue.created_at)).to eq(3) + end + it_behaves_like 'when a transaction is open' do subject { described_class.batch_count(model) } end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index a8edcc5f7e5..ff6e5437559 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1680,7 +1680,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do has_internal_id :iid, scope: :project, - init: ->(s) { s&.project&.issues&.maximum(:iid) }, + init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) }, backfill: true, presence: false end diff --git a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb new file mode 100644 index 00000000000..d47666eeffd --- /dev/null +++ b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do + include TableSchemaHelpers + + subject(:replace_table) { described_class.new(original_table, replacement_table, archived_table, 'id').perform } + + let(:original_table) { '_test_original_table' } + let(:replacement_table) { '_test_replacement_table' } + let(:archived_table) { '_test_archived_table' } + + let(:original_sequence) { "#{original_table}_id_seq" } + + let(:original_primary_key) { "#{original_table}_pkey" } + let(:replacement_primary_key) { "#{replacement_table}_pkey" } + let(:archived_primary_key) { "#{archived_table}_pkey" } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{original_table} ( + id serial NOT NULL PRIMARY KEY, + original_column text NOT NULL, + created_at timestamptz NOT NULL); + + CREATE TABLE #{replacement_table} ( + id int NOT NULL, + replacement_column text NOT NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, created_at)) + PARTITION BY RANGE (created_at); + SQL + end + + it 'replaces the current table, archiving the old' do + expect_table_to_be_replaced { replace_table } + end + + it 'transfers the primary key sequence to the replacement table' do + expect(sequence_owned_by(original_table, 'id')).to eq(original_sequence) + expect(default_expression_for(original_table, 'id')).to eq("nextval('#{original_sequence}'::regclass)") + + expect(sequence_owned_by(replacement_table, 'id')).to be_nil + expect(default_expression_for(replacement_table, 'id')).to be_nil + + expect_table_to_be_replaced { replace_table } + + expect(sequence_owned_by(original_table, 'id')).to eq(original_sequence) + expect(default_expression_for(original_table, 'id')).to eq("nextval('#{original_sequence}'::regclass)") + expect(sequence_owned_by(archived_table, 'id')).to be_nil + expect(default_expression_for(archived_table, 'id')).to be_nil + end + + it 'renames the primary key constraints to match the new table names' do + expect_primary_keys_after_tables([original_table, replacement_table]) + + expect_table_to_be_replaced { replace_table } + + expect_primary_keys_after_tables([original_table, archived_table]) + 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 + + def expect_table_to_be_replaced(&block) + super(original_table: original_table, replacement_table: replacement_table, archived_table: archived_table, &block) + end +end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb new file mode 100644 index 00000000000..7f61ff759fc --- /dev/null +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do + include TableSchemaHelpers + + let(:migration) do + ActiveRecord::Migration.new.extend(described_class) + end + + let(:table_name) { '_test_partitioned_table' } + let(:column_name) { 'created_at' } + let(:index_name) { '_test_partitioning_index_name' } + let(:partition_schema) { 'gitlab_partitions_dynamic' } + let(:partition1_identifier) { "#{partition_schema}.#{table_name}_202001" } + let(:partition2_identifier) { "#{partition_schema}.#{table_name}_202002" } + let(:partition1_index) { "index_#{table_name}_202001_#{column_name}" } + let(:partition2_index) { "index_#{table_name}_202002_#{column_name}" } + + before do + allow(migration).to receive(:puts) + + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial NOT NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, created_at) + ) PARTITION BY RANGE (created_at); + + CREATE TABLE #{partition1_identifier} PARTITION OF #{table_name} + FOR VALUES FROM ('2020-01-01') TO ('2020-02-01'); + + CREATE TABLE #{partition2_identifier} PARTITION OF #{table_name} + FOR VALUES FROM ('2020-02-01') TO ('2020-03-01'); + SQL + end + + describe '#add_concurrent_partitioned_index' do + before do + allow(migration).to receive(:index_name_exists?).with(table_name, index_name).and_return(false) + + allow(migration).to receive(:generated_index_name).and_return(partition1_index, partition2_index) + + allow(migration).to receive(:with_lock_retries).and_yield + end + + context 'when the index does not exist on the parent table' do + it 'creates the index on each partition, and the parent table', :aggregate_failures do + expect(migration).to receive(:index_name_exists?).with(table_name, index_name).and_return(false) + + expect_add_concurrent_index_and_call_original(partition1_identifier, column_name, partition1_index) + expect_add_concurrent_index_and_call_original(partition2_identifier, column_name, partition2_index) + + expect(migration).to receive(:with_lock_retries).ordered.and_yield + expect(migration).to receive(:add_index).with(table_name, column_name, name: index_name).ordered.and_call_original + + migration.add_concurrent_partitioned_index(table_name, column_name, name: index_name) + + expect_index_to_exist(partition1_index, schema: partition_schema) + expect_index_to_exist(partition2_index, schema: partition_schema) + expect_index_to_exist(index_name) + end + + def expect_add_concurrent_index_and_call_original(table, column, index) + expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, name: index) + .and_wrap_original { |_, table, column, options| connection.add_index(table, column, options) } + end + end + + context 'when the index exists on the parent table' do + it 'does not attempt to create any indexes', :aggregate_failures do + expect(migration).to receive(:index_name_exists?).with(table_name, index_name).and_return(true) + + expect(migration).not_to receive(:add_concurrent_index) + expect(migration).not_to receive(:with_lock_retries) + expect(migration).not_to receive(:add_index) + + migration.add_concurrent_partitioned_index(table_name, column_name, name: index_name) + end + end + + context 'when additional index options are given' do + before do + connection.execute(<<~SQL) + DROP TABLE #{partition2_identifier} + SQL + end + + it 'forwards them to the index helper methods', :aggregate_failures do + expect(migration).to receive(:add_concurrent_index) + .with(partition1_identifier, column_name, name: partition1_index, where: 'x > 0', unique: true) + + expect(migration).to receive(:add_index) + .with(table_name, column_name, name: index_name, where: 'x > 0', unique: true) + + migration.add_concurrent_partitioned_index(table_name, column_name, + name: index_name, where: 'x > 0', unique: true) + end + end + + context 'when a name argument for the index is not given' do + it 'raises an error', :aggregate_failures do + expect(migration).not_to receive(:add_concurrent_index) + expect(migration).not_to receive(:with_lock_retries) + expect(migration).not_to receive(:add_index) + + expect do + migration.add_concurrent_partitioned_index(table_name, column_name) + end.to raise_error(ArgumentError, /A name is required for indexes added to partitioned tables/) + end + end + + context 'when the given table is not a partitioned table' do + before do + allow(Gitlab::Database::PostgresPartitionedTable).to receive(:find_by_name_in_current_schema) + .with(table_name).and_return(nil) + end + + it 'raises an error', :aggregate_failures do + expect(migration).not_to receive(:add_concurrent_index) + expect(migration).not_to receive(:with_lock_retries) + expect(migration).not_to receive(:add_index) + + expect do + migration.add_concurrent_partitioned_index(table_name, column_name, name: index_name) + end.to raise_error(ArgumentError, /#{table_name} is not a partitioned table/) + end + end + end + + describe '#remove_concurrent_partitioned_index_by_name' do + context 'when the index exists' do + before do + connection.execute(<<~SQL) + CREATE INDEX #{partition1_index} ON #{partition1_identifier} (#{column_name}); + CREATE INDEX #{partition2_index} ON #{partition2_identifier} (#{column_name}); + + CREATE INDEX #{index_name} ON #{table_name} (#{column_name}); + SQL + end + + it 'drops the index on the parent table, cascading to all partitions', :aggregate_failures do + expect_index_to_exist(partition1_index, schema: partition_schema) + expect_index_to_exist(partition2_index, schema: partition_schema) + expect_index_to_exist(index_name) + + expect(migration).to receive(:with_lock_retries).ordered.and_yield + expect(migration).to receive(:remove_index).with(table_name, name: index_name).ordered.and_call_original + + migration.remove_concurrent_partitioned_index_by_name(table_name, index_name) + + expect_index_not_to_exist(partition1_index, schema: partition_schema) + expect_index_not_to_exist(partition2_index, schema: partition_schema) + expect_index_not_to_exist(index_name) + end + end + + context 'when the index does not exist' do + it 'does not attempt to drop the index', :aggregate_failures do + expect(migration).to receive(:index_name_exists?).with(table_name, index_name).and_return(false) + + expect(migration).not_to receive(:with_lock_retries) + expect(migration).not_to receive(:remove_index) + + migration.remove_concurrent_partitioned_index_by_name(table_name, index_name) + end + end + + context 'when the given table is not a partitioned table' do + before do + allow(Gitlab::Database::PostgresPartitionedTable).to receive(:find_by_name_in_current_schema) + .with(table_name).and_return(nil) + end + + it 'raises an error', :aggregate_failures do + expect(migration).not_to receive(:with_lock_retries) + expect(migration).not_to receive(:remove_index) + + expect do + migration.remove_concurrent_partitioned_index_by_name(table_name, index_name) + end.to raise_error(ArgumentError, /#{table_name} is not a partitioned table/) + end + end + end +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 147637cf471..f10ff704c17 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 @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers do include PartitioningHelpers include TriggerHelpers + include TableSchemaHelpers let(:migration) do ActiveRecord::Migration.new.extend(described_class) @@ -629,6 +630,76 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end + describe '#replace_with_partitioned_table' do + let(:archived_table) { "#{source_table}_archived" } + + before do + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + end + + it 'replaces the original table with the partitioned table' do + expect(table_type(source_table)).to eq('normal') + expect(table_type(partitioned_table)).to eq('partitioned') + expect(table_type(archived_table)).to be_nil + + expect_table_to_be_replaced { migration.replace_with_partitioned_table(source_table) } + + expect(table_type(source_table)).to eq('partitioned') + expect(table_type(archived_table)).to eq('normal') + expect(table_type(partitioned_table)).to be_nil + end + + it 'moves the trigger from the original table to the new table' do + expect_function_to_exist(function_name) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + + expect_table_to_be_replaced { migration.replace_with_partitioned_table(source_table) } + + expect_function_to_exist(function_name) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + end + + def expect_table_to_be_replaced(&block) + super(original_table: source_table, replacement_table: partitioned_table, archived_table: archived_table, &block) + end + end + + describe '#rollback_replace_with_partitioned_table' do + let(:archived_table) { "#{source_table}_archived" } + + before do + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + + migration.replace_with_partitioned_table source_table + end + + it 'replaces the partitioned table with the non-partitioned table' do + expect(table_type(source_table)).to eq('partitioned') + expect(table_type(archived_table)).to eq('normal') + expect(table_type(partitioned_table)).to be_nil + + expect_table_to_be_replaced { migration.rollback_replace_with_partitioned_table(source_table) } + + expect(table_type(source_table)).to eq('normal') + expect(table_type(partitioned_table)).to eq('partitioned') + expect(table_type(archived_table)).to be_nil + end + + it 'moves the trigger from the partitioned table to the non-partitioned table' do + expect_function_to_exist(function_name) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + + expect_table_to_be_replaced { migration.rollback_replace_with_partitioned_table(source_table) } + + expect_function_to_exist(function_name) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + end + + def expect_table_to_be_replaced(&block) + super(original_table: source_table, replacement_table: archived_table, archived_table: partitioned_table, &block) + end + end + def filter_columns_by_name(columns, names) columns.reject { |c| names.include?(c.name) } end diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb index 1da67a5a6c0..d65b638f7bc 100644 --- a/spec/lib/gitlab/database/postgres_index_spec.rb +++ b/spec/lib/gitlab/database/postgres_index_spec.rb @@ -3,9 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PostgresIndex do + let(:schema) { 'public' } + let(:name) { 'foo_idx' } + let(:identifier) { "#{schema}.#{name}" } + before do ActiveRecord::Base.connection.execute(<<~SQL) - CREATE INDEX foo_idx ON public.users (name); + CREATE INDEX #{name} ON public.users (name); CREATE UNIQUE INDEX bar_key ON public.users (id); CREATE TABLE example_table (id serial primary key); @@ -16,19 +20,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do described_class.by_identifier(name) end - describe '.by_identifier' do - it 'finds the index' do - expect(find('public.foo_idx')).to be_a(Gitlab::Database::PostgresIndex) - end - - it 'raises an error if not found' do - expect { find('public.idontexist') }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises ArgumentError if given a non-fully qualified index name' do - expect { find('foo') }.to raise_error(ArgumentError, /not fully qualified/) - end - end + it_behaves_like 'a postgres model' describe '.regular' do it 'only non-unique indexes' do @@ -76,7 +68,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do describe '#valid_index?' do it 'returns true if the index is invalid' do - expect(find('public.foo_idx')).to be_valid_index + expect(find(identifier)).to be_valid_index end it 'returns false if the index is marked as invalid' do @@ -86,31 +78,13 @@ RSpec.describe Gitlab::Database::PostgresIndex do WHERE pg_class.relname = 'foo_idx' AND pg_index.indexrelid = pg_class.oid SQL - expect(find('public.foo_idx')).not_to be_valid_index - end - end - - describe '#to_s' do - it 'returns the index name' do - expect(find('public.foo_idx').to_s).to eq('foo_idx') - end - end - - describe '#name' do - it 'returns the name' do - expect(find('public.foo_idx').name).to eq('foo_idx') - end - end - - describe '#schema' do - it 'returns the index schema' do - expect(find('public.foo_idx').schema).to eq('public') + expect(find(identifier)).not_to be_valid_index end end describe '#definition' do it 'returns the index definition' do - expect(find('public.foo_idx').definition).to eq('CREATE INDEX foo_idx ON public.users USING btree (name)') + expect(find(identifier).definition).to eq('CREATE INDEX foo_idx ON public.users USING btree (name)') end end end diff --git a/spec/lib/gitlab/database/postgres_partition_spec.rb b/spec/lib/gitlab/database/postgres_partition_spec.rb new file mode 100644 index 00000000000..5a44090d5ae --- /dev/null +++ b/spec/lib/gitlab/database/postgres_partition_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresPartition, type: :model do + let(:schema) { 'gitlab_partitions_dynamic' } + let(:name) { '_test_partition_01' } + let(:identifier) { "#{schema}.#{name}" } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE public._test_partitioned_table ( + id serial NOT NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, created_at) + ) PARTITION BY RANGE(created_at); + + CREATE TABLE #{identifier} PARTITION OF public._test_partitioned_table + FOR VALUES FROM ('2020-01-01') to ('2020-02-01'); + SQL + end + + def find(identifier) + described_class.by_identifier(identifier) + end + + describe 'associations' do + it { is_expected.to belong_to(:postgres_partitioned_table).with_primary_key('identifier').with_foreign_key('parent_identifier') } + end + + it_behaves_like 'a postgres model' + + describe '.for_parent_table' do + let(:second_name) { '_test_partition_02' } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE #{schema}.#{second_name} PARTITION OF public._test_partitioned_table + FOR VALUES FROM ('2020-02-01') to ('2020-03-01'); + + CREATE TABLE #{schema}._test_other_table ( + id serial NOT NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, created_at) + ) PARTITION BY RANGE(created_at); + + CREATE TABLE #{schema}._test_other_partition_01 PARTITION OF #{schema}._test_other_table + FOR VALUES FROM ('2020-01-01') to ('2020-02-01'); + SQL + end + + it 'returns partitions for the parent table in the current schema' do + partitions = described_class.for_parent_table('_test_partitioned_table') + + expect(partitions.count).to eq(2) + expect(partitions.pluck(:name)).to eq([name, second_name]) + end + + it 'does not return partitions for tables not in the current schema' do + expect(described_class.for_parent_table('_test_other_table').count).to eq(0) + end + end + + describe '#parent_identifier' do + it 'returns the parent table identifier' do + expect(find(identifier).parent_identifier).to eq('public._test_partitioned_table') + end + end + + describe '#condition' do + it 'returns the condition for the partitioned values' do + expect(find(identifier).condition).to eq("FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')") + end + end +end diff --git a/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb b/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb new file mode 100644 index 00000000000..21a46f1a0a6 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresPartitionedTable, type: :model do + let(:schema) { 'public' } + let(:name) { 'foo_range' } + let(:identifier) { "#{schema}.#{name}" } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE #{identifier} ( + id serial NOT NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, created_at) + ) PARTITION BY RANGE(created_at); + + CREATE TABLE public.foo_list ( + id serial NOT NULL, + row_type text NOT NULL, + PRIMARY KEY (id, row_type) + ) PARTITION BY LIST(row_type); + + CREATE TABLE public.foo_hash ( + id serial NOT NULL, + row_value int NOT NULL, + PRIMARY KEY (id, row_value) + ) PARTITION BY HASH (row_value); + SQL + end + + def find(identifier) + described_class.by_identifier(identifier) + end + + describe 'associations' do + it { is_expected.to have_many(:postgres_partitions).with_primary_key('identifier').with_foreign_key('parent_identifier') } + end + + it_behaves_like 'a postgres model' + + describe '.find_by_name_in_current_schema' do + it 'finds the partitioned tables in the current schema by name', :aggregate_failures do + partitioned_table = described_class.find_by_name_in_current_schema(name) + + expect(partitioned_table).not_to be_nil + expect(partitioned_table.identifier).to eq(identifier) + end + + it 'does not find partitioned tables in a different schema' do + ActiveRecord::Base.connection.execute(<<~SQL) + ALTER TABLE #{identifier} SET SCHEMA gitlab_partitions_dynamic + SQL + + expect(described_class.find_by_name_in_current_schema(name)).to be_nil + end + end + + describe '#dynamic?' do + it 'returns true for tables partitioned by range' do + expect(find('public.foo_range')).to be_dynamic + end + + it 'returns true for tables partitioned by list' do + expect(find('public.foo_list')).to be_dynamic + end + + it 'returns false for tables partitioned by hash' do + expect(find('public.foo_hash')).not_to be_dynamic + end + end + + describe '#static?' do + it 'returns false for tables partitioned by range' do + expect(find('public.foo_range')).not_to be_static + end + + it 'returns false for tables partitioned by list' do + expect(find('public.foo_list')).not_to be_static + end + + it 'returns true for tables partitioned by hash' do + expect(find('public.foo_hash')).to be_static + end + end + + describe '#strategy' do + it 'returns the partitioning strategy' do + expect(find(identifier).strategy).to eq('range') + end + end + + describe '#key_columns' do + it 'returns the partitioning key columns' do + expect(find(identifier).key_columns).to match_array(['created_at']) + end + end +end diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index 86b3c029944..359e0597f4e 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Database::Reindexing do it 'retrieves regular indexes that are no left-overs from previous runs' do result = double - expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.not_match.not_match').with(no_args).with('^tmp_reindex_').with('^old_reindex_').and_return(result) + expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.where.not_match.not_match').with(no_args).with('NOT expression').with('^tmp_reindex_').with('^old_reindex_').and_return(result) expect(subject).to eq(result) end |