From 44de03857a3db8d7fbd5e7ca48b69f9afaffc868 Mon Sep 17 00:00:00 2001 From: Simon Tomlinson <stomlinson@gitlab.com> Date: Tue, 27 Jul 2021 11:36:00 +0000 Subject: [PATCH] Add a database view for postgres foreign keys --- .../20210719145532_add_foreign_keys_view.rb | 26 ++++++++++++ db/schema_migrations/20210719145532 | 1 + db/structure.sql | 12 ++++++ lib/gitlab/database/postgres_foreign_key.rb | 15 +++++++ .../database/postgres_foreign_key_spec.rb | 41 +++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 db/migrate/20210719145532_add_foreign_keys_view.rb create mode 100644 db/schema_migrations/20210719145532 create mode 100644 lib/gitlab/database/postgres_foreign_key.rb create mode 100644 spec/lib/gitlab/database/postgres_foreign_key_spec.rb diff --git a/db/migrate/20210719145532_add_foreign_keys_view.rb b/db/migrate/20210719145532_add_foreign_keys_view.rb new file mode 100644 index 000000000000..2d31371e7820 --- /dev/null +++ b/db/migrate/20210719145532_add_foreign_keys_view.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AddForeignKeysView < ActiveRecord::Migration[6.1] + def up + execute(<<~SQL) + CREATE OR REPLACE VIEW postgres_foreign_keys AS + SELECT + pg_constraint.oid AS oid, + pg_constraint.conname AS name, + constrained_namespace.nspname::text || '.'::text || constrained_table.relname::text AS constrained_table_identifier, + referenced_namespace.nspname::text || '.'::text || referenced_table.relname::text AS referenced_table_identifier + FROM pg_constraint + INNER JOIN pg_class constrained_table ON constrained_table.oid = pg_constraint.conrelid + INNER JOIN pg_class referenced_table ON referenced_table.oid = pg_constraint.confrelid + INNER JOIN pg_namespace constrained_namespace ON constrained_table.relnamespace = constrained_namespace.oid + INNER JOIN pg_namespace referenced_namespace ON referenced_table.relnamespace = referenced_namespace.oid + WHERE contype = 'f'; + SQL + end + + def down + execute(<<~SQL) + DROP VIEW IF EXISTS postgres_foreign_keys + SQL + end +end diff --git a/db/schema_migrations/20210719145532 b/db/schema_migrations/20210719145532 new file mode 100644 index 000000000000..a9afd7a18ed4 --- /dev/null +++ b/db/schema_migrations/20210719145532 @@ -0,0 +1 @@ +5e088e5109b50d8f4fadd37a0382d7dc4ce856a851ec2b97f8d5d868c3cb19fd \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6075cd812f98..c5d2b0806d65 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -16519,6 +16519,18 @@ CREATE SEQUENCE pool_repositories_id_seq ALTER SEQUENCE pool_repositories_id_seq OWNED BY pool_repositories.id; +CREATE VIEW postgres_foreign_keys AS + SELECT pg_constraint.oid, + pg_constraint.conname AS name, + (((constrained_namespace.nspname)::text || '.'::text) || (constrained_table.relname)::text) AS constrained_table_identifier, + (((referenced_namespace.nspname)::text || '.'::text) || (referenced_table.relname)::text) AS referenced_table_identifier + FROM ((((pg_constraint + JOIN pg_class constrained_table ON ((constrained_table.oid = pg_constraint.conrelid))) + JOIN pg_class referenced_table ON ((referenced_table.oid = pg_constraint.confrelid))) + JOIN pg_namespace constrained_namespace ON ((constrained_table.relnamespace = constrained_namespace.oid))) + JOIN pg_namespace referenced_namespace ON ((referenced_table.relnamespace = referenced_namespace.oid))) + WHERE (pg_constraint.contype = 'f'::"char"); + CREATE VIEW postgres_index_bloat_estimates AS SELECT (((relation_stats.nspname)::text || '.'::text) || (relation_stats.idxname)::text) AS identifier, ( diff --git a/lib/gitlab/database/postgres_foreign_key.rb b/lib/gitlab/database/postgres_foreign_key.rb new file mode 100644 index 000000000000..94f747242955 --- /dev/null +++ b/lib/gitlab/database/postgres_foreign_key.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Database + class PostgresForeignKey < ApplicationRecord + self.primary_key = :oid + + scope :by_referenced_table_identifier, ->(identifier) do + raise ArgumentError, "Referenced table name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/ + + where(referenced_table_identifier: identifier) + end + end + end +end diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb new file mode 100644 index 000000000000..ec39e5bfee73 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model do + # PostgresForeignKey does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry + # in pg_class + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE public.referenced_table ( + id bigserial primary key not null + ); + + CREATE TABLE public.other_referenced_table ( + id bigserial primary key not null + ); + + CREATE TABLE public.constrained_table ( + id bigserial primary key not null, + referenced_table_id bigint not null, + other_referenced_table_id bigint not null, + CONSTRAINT fk_constrained_to_referenced FOREIGN KEY(referenced_table_id) REFERENCES referenced_table(id), + CONSTRAINT fk_constrained_to_other_referenced FOREIGN KEY(other_referenced_table_id) + REFERENCES other_referenced_table(id) + ); + SQL + end + + describe '#by_referenced_table_identifier' do + it 'throws an error when the identifier name is not fully qualified' do + expect { described_class.by_referenced_table_identifier('referenced_table') }.to raise_error(ArgumentError, /not fully qualified/) + end + + it 'finds the foreign keys for the referenced table' do + expected = described_class.find_by!(name: 'fk_constrained_to_referenced') + + expect(described_class.by_referenced_table_identifier('public.referenced_table')).to contain_exactly(expected) + end + end +end -- GitLab