From c021b8f5f69c33c3c45c2c07566d8dd61d03bca5 Mon Sep 17 00:00:00 2001 From: pbair <pbair@gitlab.com> Date: Wed, 16 Dec 2020 14:41:37 -0500 Subject: [PATCH] Move database spec helpers into separate directory We'd like to run CI pipelines against officially supported PG versions to ensure we're compatible with all of them. Since this isn't practical to do for every MR created, we'd like to trigger those tests only when specific files are edited that are likely to work differently across versions. We have a number of spec helpers for database, so this moves them all into their own directory where it's easy to detect a file change when the pipeline runs. --- .../index_helpers.rb | 2 +- spec/factories_spec.rb | 2 +- .../partitioning/partition_creator_spec.rb | 2 +- .../partitioning/replace_table_spec.rb | 2 +- .../foreign_key_helpers_spec.rb | 2 +- .../index_helpers_spec.rb | 2 +- .../table_management_helpers_spec.rb | 6 +- .../reindexing/index_selection_spec.rb | 2 +- .../helpers/database/database_helpers.rb | 15 +++ .../helpers/database/partitioning_helpers.rb | 96 +++++++++++++++ .../helpers/database/table_schema_helpers.rb | 114 ++++++++++++++++++ .../helpers/database/trigger_helpers.rb | 68 +++++++++++ spec/support/helpers/database_helpers.rb | 13 -- spec/support/helpers/partitioning_helpers.rb | 94 --------------- spec/support/helpers/table_schema_helpers.rb | 112 ----------------- spec/support/helpers/trigger_helpers.rb | 66 ---------- 16 files changed, 303 insertions(+), 295 deletions(-) create mode 100644 spec/support/helpers/database/database_helpers.rb create mode 100644 spec/support/helpers/database/partitioning_helpers.rb create mode 100644 spec/support/helpers/database/table_schema_helpers.rb create mode 100644 spec/support/helpers/database/trigger_helpers.rb delete mode 100644 spec/support/helpers/database_helpers.rb delete mode 100644 spec/support/helpers/partitioning_helpers.rb delete mode 100644 spec/support/helpers/table_schema_helpers.rb delete mode 100644 spec/support/helpers/trigger_helpers.rb diff --git a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb index f367292f4b0a..0bc1343acca1 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb @@ -32,7 +32,7 @@ def add_concurrent_partitioned_index(table_name, column_names, options = {}) return end - partitioned_table.postgres_partitions.each do |partition| + partitioned_table.postgres_partitions.order(:name).each do |partition| partition_index_name = generated_index_name(partition.identifier, options[:name]) partition_options = options.merge(name: partition_index_name) diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 1afd5bdb2cad..38ade20de286 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'factories' do - include DatabaseHelpers + include Database::DatabaseHelpers shared_examples 'factory' do |factory| describe "#{factory.name} factory" do diff --git a/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb b/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb index 563999416628..ec89f2ed61c7 100644 --- a/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do - include PartitioningHelpers + include Database::PartitioningHelpers include ExclusiveLeaseHelpers describe '.register' do diff --git a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb index d47666eeffd2..8e27797208c7 100644 --- a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do - include TableSchemaHelpers + include Database::TableSchemaHelpers subject(:replace_table) { described_class.new(original_table, replacement_table, archived_table, 'id').perform } diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb index 7d88c17c9b37..93dbd9d7c300 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do - include TriggerHelpers + include Database::TriggerHelpers let(:model) do ActiveRecord::Migration.new.extend(described_class) 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 index 7f61ff759fc8..603f3dc41af5 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do - include TableSchemaHelpers + include Database::TableSchemaHelpers let(:migration) do ActiveRecord::Migration.new.extend(described_class) 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 f10ff704c178..5fdb7dd9a85f 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 @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers do - include PartitioningHelpers - include TriggerHelpers - include TableSchemaHelpers + include Database::PartitioningHelpers + include Database::TriggerHelpers + include Database::TableSchemaHelpers let(:migration) do ActiveRecord::Migration.new.extend(described_class) diff --git a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb index a5e2f368f406..4466679a099e 100644 --- a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb +++ b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Reindexing::IndexSelection do - include DatabaseHelpers + include Database::DatabaseHelpers subject { described_class.new(Gitlab::Database::PostgresIndex.all).to_a } diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb new file mode 100644 index 000000000000..b8d7ea3662f9 --- /dev/null +++ b/spec/support/helpers/database/database_helpers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Database + module DatabaseHelpers + # In order to directly work with views using factories, + # we can swapout the view for a table of identical structure. + def swapout_view_for_table(view) + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE #{view}_copy (LIKE #{view}); + DROP VIEW #{view}; + ALTER TABLE #{view}_copy RENAME TO #{view}; + SQL + end + end +end diff --git a/spec/support/helpers/database/partitioning_helpers.rb b/spec/support/helpers/database/partitioning_helpers.rb new file mode 100644 index 000000000000..80b31fe06032 --- /dev/null +++ b/spec/support/helpers/database/partitioning_helpers.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module Database + module PartitioningHelpers + def expect_table_partitioned_by(table, columns, part_type: :range) + columns_with_part_type = columns.map { |c| [part_type.to_s, c] } + actual_columns = find_partitioned_columns(table) + + 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, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") + end + + def expect_total_partitions(table_name, count, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + partitions = find_partitions(table_name, schema: schema) + + expect(partitions.size).to eq(count) + end + + def expect_range_partitions_for(table_name, partitions) + partitions.each do |suffix, (min_value, max_value)| + partition_name = "#{table_name}_#{suffix}" + expect_range_partition_of(partition_name, table_name, min_value, max_value) + end + + expect_total_partitions(table_name, partitions.size, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + end + + def expect_hash_partition_of(partition_name, table_name, modulus, remainder) + definition = find_partition_definition(partition_name, schema: Gitlab::Database::STATIC_PARTITIONS_SCHEMA) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES WITH (modulus #{modulus}, remainder #{remainder})") + end + + private + + def find_partitioned_columns(table) + connection.select_rows(<<~SQL) + select + case partstrat + when 'l' then 'list' + when 'r' then 'range' + when 'h' then 'hash' + end as partstrat, + cols.column_name + from ( + select partrelid, partstrat, unnest(partattrs) as col_pos + from pg_partitioned_table + ) pg_part + inner join pg_class + on pg_part.partrelid = pg_class.oid + inner join information_schema.columns cols + on cols.table_name = pg_class.relname + and cols.ordinal_position = pg_part.col_pos + where pg_class.relname = '#{table}'; + SQL + end + + def find_partition_definition(partition, schema: ) + connection.select_one(<<~SQL) + select + parent_class.relname as base_table, + pg_get_expr(pg_class.relpartbound, inhrelid) as condition + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + 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 + + def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + connection.select_rows(<<~SQL) + select + pg_class.relname + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and parent_class.relname = '#{partition}' + and pg_class.relispartition + SQL + end + end +end diff --git a/spec/support/helpers/database/table_schema_helpers.rb b/spec/support/helpers/database/table_schema_helpers.rb new file mode 100644 index 000000000000..854d2c94df2b --- /dev/null +++ b/spec/support/helpers/database/table_schema_helpers.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Database + module TableSchemaHelpers + def connection + ActiveRecord::Base.connection + end + + def expect_table_to_be_replaced(original_table:, replacement_table:, archived_table:) + original_oid = table_oid(original_table) + replacement_oid = table_oid(replacement_table) + + yield + + expect(table_oid(original_table)).to eq(replacement_oid) + expect(table_oid(archived_table)).to eq(original_oid) + expect(table_oid(replacement_table)).to be_nil + end + + def expect_index_to_exist(name, schema: nil) + expect(index_exists_by_name(name, schema: schema)).to eq(true) + end + + def expect_index_not_to_exist(name, schema: nil) + expect(index_exists_by_name(name, schema: schema)).to be_nil + 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) + connection.select_value(<<~SQL) + SELECT oid + FROM pg_catalog.pg_class + WHERE relname = '#{name}' + SQL + end + + def table_type(name) + connection.select_value(<<~SQL) + SELECT + CASE class.relkind + WHEN 'r' THEN 'normal' + WHEN 'p' THEN 'partitioned' + ELSE 'other' + END as table_type + FROM pg_catalog.pg_class class + WHERE class.relname = '#{name}' + SQL + end + + def sequence_owned_by(table_name, column_name) + connection.select_value(<<~SQL) + SELECT + sequence.relname as name + FROM pg_catalog.pg_class as sequence + INNER JOIN pg_catalog.pg_depend depend + ON depend.objid = sequence.oid + INNER JOIN pg_catalog.pg_class class + ON class.oid = depend.refobjid + INNER JOIN pg_catalog.pg_attribute attribute + ON attribute.attnum = depend.refobjsubid + AND attribute.attrelid = depend.refobjid + WHERE class.relname = '#{table_name}' + AND attribute.attname = '#{column_name}' + SQL + end + + def default_expression_for(table_name, column_name) + connection.select_value(<<~SQL) + SELECT + pg_get_expr(attrdef.adbin, attrdef.adrelid) AS default_value + FROM pg_catalog.pg_attribute attribute + INNER JOIN pg_catalog.pg_attrdef attrdef + ON attribute.attrelid = attrdef.adrelid + AND attribute.attnum = attrdef.adnum + WHERE attribute.attrelid = '#{table_name}'::regclass + AND attribute.attname = '#{column_name}' + SQL + end + + def primary_key_constraint_name(table_name, schema: nil) + table_name = schema ? "#{schema}.#{table_name}" : table_name + + connection.select_value(<<~SQL) + SELECT + conname AS constraint_name + FROM pg_catalog.pg_constraint + WHERE pg_constraint.conrelid = '#{table_name}'::regclass + AND pg_constraint.contype = 'p' + SQL + end + + def index_exists_by_name(index, schema: nil) + schema = schema ? "'#{schema}'" : 'current_schema' + + connection.select_value(<<~SQL) + SELECT true + FROM pg_catalog.pg_index i + INNER JOIN pg_catalog.pg_class c + ON c.oid = i.indexrelid + INNER JOIN pg_catalog.pg_namespace n + ON c.relnamespace = n.oid + WHERE c.relname = '#{index}' + AND n.nspname = #{schema} + SQL + end + end +end diff --git a/spec/support/helpers/database/trigger_helpers.rb b/spec/support/helpers/database/trigger_helpers.rb new file mode 100644 index 000000000000..9ec03e684138 --- /dev/null +++ b/spec/support/helpers/database/trigger_helpers.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Database + module TriggerHelpers + def expect_function_to_exist(name) + expect(find_function_def(name)).not_to be_nil + end + + def expect_function_not_to_exist(name) + expect(find_function_def(name)).to be_nil + end + + def expect_function_to_contain(name, *statements) + return_stmt, *body_stmts = parsed_function_statements(name).reverse + + expect(return_stmt).to eq('return old') + expect(body_stmts).to contain_exactly(*statements) + end + + def expect_trigger_not_to_exist(table_name, name) + expect(find_trigger_def(table_name, name)).to be_nil + end + + def expect_valid_function_trigger(table_name, name, fn_name, fires_on) + events, timing, definition = cleaned_trigger_def(table_name, name) + + events = events&.split(',') + expected_timing, expected_events = fires_on.first + expect(timing).to eq(expected_timing.to_s) + expect(events).to match_array(Array.wrap(expected_events)) + + expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) + end + + private + + def parsed_function_statements(name) + cleaned_definition = find_function_def(name)['body'].downcase.gsub(/\s+/, ' ') + statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1") + statements.split(';').map! { |stmt| stmt.strip.presence }.compact! + end + + def find_function_def(name) + connection.select_one(<<~SQL) + SELECT prosrc AS body + FROM pg_proc + WHERE proname = '#{name}' + SQL + end + + def cleaned_trigger_def(table_name, name) + find_trigger_def(table_name, name).values_at('event', 'action_timing', 'action_statement').map!(&:downcase) + end + + def find_trigger_def(table_name, name) + connection.select_one(<<~SQL) + SELECT + string_agg(event_manipulation, ',') AS event, + action_timing, + action_statement + FROM information_schema.triggers + WHERE event_object_table = '#{table_name}' + AND trigger_name = '#{name}' + GROUP BY 2, 3 + SQL + end + end +end diff --git a/spec/support/helpers/database_helpers.rb b/spec/support/helpers/database_helpers.rb deleted file mode 100644 index e9f0a74a8d10..000000000000 --- a/spec/support/helpers/database_helpers.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module DatabaseHelpers - # In order to directly work with views using factories, - # we can swapout the view for a table of identical structure. - def swapout_view_for_table(view) - ActiveRecord::Base.connection.execute(<<~SQL) - CREATE TABLE #{view}_copy (LIKE #{view}); - DROP VIEW #{view}; - ALTER TABLE #{view}_copy RENAME TO #{view}; - SQL - end -end diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb deleted file mode 100644 index 8981fea04d53..000000000000 --- a/spec/support/helpers/partitioning_helpers.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -module PartitioningHelpers - def expect_table_partitioned_by(table, columns, part_type: :range) - columns_with_part_type = columns.map { |c| [part_type.to_s, c] } - actual_columns = find_partitioned_columns(table) - - 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, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - - expect(definition).not_to be_nil - expect(definition['base_table']).to eq(table_name.to_s) - expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") - end - - def expect_total_partitions(table_name, count, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - partitions = find_partitions(table_name, schema: schema) - - expect(partitions.size).to eq(count) - end - - def expect_range_partitions_for(table_name, partitions) - partitions.each do |suffix, (min_value, max_value)| - partition_name = "#{table_name}_#{suffix}" - expect_range_partition_of(partition_name, table_name, min_value, max_value) - end - - expect_total_partitions(table_name, partitions.size, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - end - - def expect_hash_partition_of(partition_name, table_name, modulus, remainder) - definition = find_partition_definition(partition_name, schema: Gitlab::Database::STATIC_PARTITIONS_SCHEMA) - - expect(definition).not_to be_nil - expect(definition['base_table']).to eq(table_name.to_s) - expect(definition['condition']).to eq("FOR VALUES WITH (modulus #{modulus}, remainder #{remainder})") - end - - private - - def find_partitioned_columns(table) - connection.select_rows(<<~SQL) - select - case partstrat - when 'l' then 'list' - when 'r' then 'range' - when 'h' then 'hash' - end as partstrat, - cols.column_name - from ( - select partrelid, partstrat, unnest(partattrs) as col_pos - from pg_partitioned_table - ) pg_part - inner join pg_class - on pg_part.partrelid = pg_class.oid - inner join information_schema.columns cols - on cols.table_name = pg_class.relname - and cols.ordinal_position = pg_part.col_pos - where pg_class.relname = '#{table}'; - SQL - end - - def find_partition_definition(partition, schema: ) - connection.select_one(<<~SQL) - select - parent_class.relname as base_table, - pg_get_expr(pg_class.relpartbound, inhrelid) as condition - from pg_class - inner join pg_inherits i on pg_class.oid = inhrelid - inner join pg_class parent_class on parent_class.oid = inhparent - 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 - - def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - connection.select_rows(<<~SQL) - select - pg_class.relname - from pg_class - inner join pg_inherits i on pg_class.oid = inhrelid - inner join pg_class parent_class on parent_class.oid = inhparent - inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace - where pg_namespace.nspname = '#{schema}' - and parent_class.relname = '#{partition}' - and pg_class.relispartition - SQL - end -end diff --git a/spec/support/helpers/table_schema_helpers.rb b/spec/support/helpers/table_schema_helpers.rb deleted file mode 100644 index 287942111905..000000000000 --- a/spec/support/helpers/table_schema_helpers.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module TableSchemaHelpers - def connection - ActiveRecord::Base.connection - end - - def expect_table_to_be_replaced(original_table:, replacement_table:, archived_table:) - original_oid = table_oid(original_table) - replacement_oid = table_oid(replacement_table) - - yield - - expect(table_oid(original_table)).to eq(replacement_oid) - expect(table_oid(archived_table)).to eq(original_oid) - expect(table_oid(replacement_table)).to be_nil - end - - def expect_index_to_exist(name, schema: nil) - expect(index_exists_by_name(name, schema: schema)).to eq(true) - end - - def expect_index_not_to_exist(name, schema: nil) - expect(index_exists_by_name(name, schema: schema)).to be_nil - 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) - connection.select_value(<<~SQL) - SELECT oid - FROM pg_catalog.pg_class - WHERE relname = '#{name}' - SQL - end - - def table_type(name) - connection.select_value(<<~SQL) - SELECT - CASE class.relkind - WHEN 'r' THEN 'normal' - WHEN 'p' THEN 'partitioned' - ELSE 'other' - END as table_type - FROM pg_catalog.pg_class class - WHERE class.relname = '#{name}' - SQL - end - - def sequence_owned_by(table_name, column_name) - connection.select_value(<<~SQL) - SELECT - sequence.relname as name - FROM pg_catalog.pg_class as sequence - INNER JOIN pg_catalog.pg_depend depend - ON depend.objid = sequence.oid - INNER JOIN pg_catalog.pg_class class - ON class.oid = depend.refobjid - INNER JOIN pg_catalog.pg_attribute attribute - ON attribute.attnum = depend.refobjsubid - AND attribute.attrelid = depend.refobjid - WHERE class.relname = '#{table_name}' - AND attribute.attname = '#{column_name}' - SQL - end - - def default_expression_for(table_name, column_name) - connection.select_value(<<~SQL) - SELECT - pg_get_expr(attrdef.adbin, attrdef.adrelid) AS default_value - FROM pg_catalog.pg_attribute attribute - INNER JOIN pg_catalog.pg_attrdef attrdef - ON attribute.attrelid = attrdef.adrelid - AND attribute.attnum = attrdef.adnum - WHERE attribute.attrelid = '#{table_name}'::regclass - AND attribute.attname = '#{column_name}' - SQL - end - - def primary_key_constraint_name(table_name, schema: nil) - table_name = schema ? "#{schema}.#{table_name}" : table_name - - connection.select_value(<<~SQL) - SELECT - conname AS constraint_name - FROM pg_catalog.pg_constraint - WHERE pg_constraint.conrelid = '#{table_name}'::regclass - AND pg_constraint.contype = 'p' - SQL - end - - def index_exists_by_name(index, schema: nil) - schema = schema ? "'#{schema}'" : 'current_schema' - - connection.select_value(<<~SQL) - SELECT true - FROM pg_catalog.pg_index i - INNER JOIN pg_catalog.pg_class c - ON c.oid = i.indexrelid - INNER JOIN pg_catalog.pg_namespace n - ON c.relnamespace = n.oid - WHERE c.relname = '#{index}' - AND n.nspname = #{schema} - SQL - end -end diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb deleted file mode 100644 index dd6d8ff5bb56..000000000000 --- a/spec/support/helpers/trigger_helpers.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -module TriggerHelpers - def expect_function_to_exist(name) - expect(find_function_def(name)).not_to be_nil - end - - def expect_function_not_to_exist(name) - expect(find_function_def(name)).to be_nil - end - - def expect_function_to_contain(name, *statements) - return_stmt, *body_stmts = parsed_function_statements(name).reverse - - expect(return_stmt).to eq('return old') - expect(body_stmts).to contain_exactly(*statements) - end - - def expect_trigger_not_to_exist(table_name, name) - expect(find_trigger_def(table_name, name)).to be_nil - end - - def expect_valid_function_trigger(table_name, name, fn_name, fires_on) - events, timing, definition = cleaned_trigger_def(table_name, name) - - events = events&.split(',') - expected_timing, expected_events = fires_on.first - expect(timing).to eq(expected_timing.to_s) - expect(events).to match_array(Array.wrap(expected_events)) - - expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) - end - - private - - def parsed_function_statements(name) - cleaned_definition = find_function_def(name)['body'].downcase.gsub(/\s+/, ' ') - statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1") - statements.split(';').map! { |stmt| stmt.strip.presence }.compact! - end - - def find_function_def(name) - connection.select_one(<<~SQL) - SELECT prosrc AS body - FROM pg_proc - WHERE proname = '#{name}' - SQL - end - - def cleaned_trigger_def(table_name, name) - find_trigger_def(table_name, name).values_at('event', 'action_timing', 'action_statement').map!(&:downcase) - end - - def find_trigger_def(table_name, name) - connection.select_one(<<~SQL) - SELECT - string_agg(event_manipulation, ',') AS event, - action_timing, - action_statement - FROM information_schema.triggers - WHERE event_object_table = '#{table_name}' - AND trigger_name = '#{name}' - GROUP BY 2, 3 - SQL - end -end -- GitLab