This instance will be upgraded to Heptapod 0.27.1 on 2021-12-02 at 15:00 UTC+1 (a few minutes of down time)

Unverified Commit c63e3221 authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Add many foreign keys to the projects table

This removes the need for relying on Rails' "dependent" option for data
removal, which is _incredibly_ slow (even when using :delete_all) when
deleting large amounts of data. This also ensures data consistency is
enforced on DB level and not on application level (something Rails is
really bad at).

This commit also includes various migrations to add foreign keys to
tables that eventually point to "projects" to ensure no rows get
orphaned upon removing a project.
parent 6a1557f55e49
...@@ -25,7 +25,7 @@ class Issue < ActiveRecord::Base ...@@ -25,7 +25,7 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues'
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
......
...@@ -12,7 +12,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -12,7 +12,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') } -> { order('merge_request_diffs.id DESC') }
...@@ -20,7 +20,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -20,7 +20,7 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues'
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
......
...@@ -59,6 +59,7 @@ def set_last_repository_updated_at ...@@ -59,6 +59,7 @@ def set_last_repository_updated_at
update_column(:last_repository_updated_at, self.created_at) update_column(:last_repository_updated_at, self.created_at)
end end
before_destroy :remove_private_deploy_keys
after_destroy :remove_pages after_destroy :remove_pages
# update visibility_level of forks # update visibility_level of forks
...@@ -80,96 +81,108 @@ def set_last_repository_updated_at ...@@ -80,96 +81,108 @@ def set_last_repository_updated_at
belongs_to :namespace belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy has_many :boards, before_add: :validate_board_limit
# Project services # Project services
has_one :campfire_service, dependent: :destroy has_one :campfire_service
has_one :drone_ci_service, dependent: :destroy has_one :drone_ci_service
has_one :emails_on_push_service, dependent: :destroy has_one :emails_on_push_service
has_one :pipelines_email_service, dependent: :destroy has_one :pipelines_email_service
has_one :irker_service, dependent: :destroy has_one :irker_service
has_one :pivotaltracker_service, dependent: :destroy has_one :pivotaltracker_service
has_one :hipchat_service, dependent: :destroy has_one :hipchat_service
has_one :flowdock_service, dependent: :destroy has_one :flowdock_service
has_one :assembla_service, dependent: :destroy has_one :assembla_service
has_one :asana_service, dependent: :destroy has_one :asana_service
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service
has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_slash_commands_service
has_one :mattermost_service, dependent: :destroy has_one :mattermost_service
has_one :slack_slash_commands_service, dependent: :destroy has_one :slack_slash_commands_service
has_one :slack_service, dependent: :destroy has_one :slack_service
has_one :buildkite_service, dependent: :destroy has_one :buildkite_service
has_one :bamboo_service, dependent: :destroy has_one :bamboo_service
has_one :teamcity_service, dependent: :destroy has_one :teamcity_service
has_one :pushover_service, dependent: :destroy has_one :pushover_service
has_one :jira_service, dependent: :destroy has_one :jira_service
has_one :redmine_service, dependent: :destroy has_one :redmine_service
has_one :custom_issue_tracker_service, dependent: :destroy has_one :custom_issue_tracker_service
has_one :bugzilla_service, dependent: :destroy has_one :bugzilla_service
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :gitlab_issue_tracker_service, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy has_one :external_wiki_service
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :kubernetes_service, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy has_one :mock_ci_service
has_one :mock_deployment_service, dependent: :destroy has_one :mock_deployment_service
has_one :mock_monitoring_service, dependent: :destroy has_one :mock_monitoring_service
has_one :microsoft_teams_service, dependent: :destroy has_one :microsoft_teams_service
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id" has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project has_many :forks, through: :forked_project_links, source: :forked_to_project
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
has_many :issues, dependent: :destroy has_many :issues
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :labels, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy has_many :services
has_many :events, dependent: :destroy has_many :events
has_many :milestones, dependent: :destroy has_many :milestones
has_many :notes, dependent: :destroy has_many :notes
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy has_many :protected_branches
has_many :protected_tags, dependent: :destroy has_many :protected_tags
has_many :project_authorizations has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all
alias_method :members, :project_members alias_method :members, :project_members
has_many :users, through: :project_members has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember' has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all
has_many :deploy_keys_projects, dependent: :destroy has_many :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects
has_many :starrers, through: :users_star_projects, source: :user has_many :starrers, through: :users_star_projects, source: :user
has_many :releases, dependent: :destroy has_many :releases
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy has_many :project_group_links
has_many :invited_groups, through: :project_group_links, source: :group has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains, dependent: :destroy has_many :pages_domains
has_many :todos, dependent: :destroy has_many :todos
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, as: :source, dependent: :delete_all
has_one :import_data, class_name: 'ProjectImportData'
has_one :project_feature
has_one :statistics, class_name: 'ProjectStatistics'
has_one :import_data, dependent: :delete, class_name: 'ProjectImportData' # Container repositories need to remove data from the container registry,
has_one :project_feature, dependent: :destroy # which is not managed by the DB. Hence we're still using dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete # here.
has_many :container_repositories, dependent: :destroy has_many :container_repositories, dependent: :destroy
has_many :commit_statuses, dependent: :destroy has_many :commit_statuses
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline' has_many :pipelines, class_name: 'Ci::Pipeline'
has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' # Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', dependent: :destroy
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable' has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy has_many :environments
has_many :deployments, dependent: :destroy has_many :deployments
has_many :pipeline_schedules, dependent: :destroy, class_name: 'Ci::PipelineSchedule' has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
...@@ -1240,7 +1253,13 @@ def public_pages_path ...@@ -1240,7 +1253,13 @@ def public_pages_path
File.join(pages_path, 'public') File.join(pages_path, 'public')
end end
def remove_private_deploy_keys
deploy_keys.where(public: false).delete_all
end
def remove_pages def remove_pages
::Projects::UpdatePagesConfigurationService.new(self).execute
# 1. We rename pages to temporary directory # 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching # 2. We wait 5 minutes, due to NFS caching
# 3. We asynchronously remove pages with force # 3. We asynchronously remove pages with force
......
---
title: Speed up project removals by adding foreign keys with cascading deletes to various tables
merge_request:
author:
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
CONCURRENCY = 4
disable_ddl_transaction!
# The tables/columns for which to remove orphans and add foreign keys. Order
# matters as some tables/columns should be processed before others.
TABLES = [
[:boards, :projects, :project_id],
[:lists, :labels, :label_id],
[:lists, :boards, :board_id],
[:services, :projects, :project_id],
[:forked_project_links, :projects, :forked_to_project_id],
[:merge_requests, :projects, :target_project_id],
[:labels, :projects, :project_id],
[:issues, :projects, :project_id],
[:events, :projects, :project_id],
[:milestones, :projects, :project_id],
[:notes, :projects, :project_id],
[:snippets, :projects, :project_id],
[:web_hooks, :projects, :project_id],
[:protected_branch_merge_access_levels, :protected_branches, :protected_branch_id],
[:protected_branch_push_access_levels, :protected_branches, :protected_branch_id],
[:protected_branches, :projects, :project_id],
[:protected_tags, :projects, :project_id],
[:deploy_keys_projects, :projects, :project_id],
[:users_star_projects, :projects, :project_id],
[:releases, :projects, :project_id],
[:project_group_links, :projects, :project_id],
[:pages_domains, :projects, :project_id],
[:todos, :projects, :project_id],
[:project_import_data, :projects, :project_id],
[:project_features, :projects, :project_id],
[:ci_builds, :projects, :project_id],
[:ci_pipelines, :projects, :project_id],
[:ci_runner_projects, :projects, :project_id],
[:ci_triggers, :projects, :project_id],
[:environments, :projects, :project_id],
[:deployments, :projects, :project_id]
]
def up
# These existing foreign keys don't have an "ON DELETE CASCADE" clause.
remove_foreign_key_without_error(:boards, :project_id)
remove_foreign_key_without_error(:lists, :label_id)
remove_foreign_key_without_error(:lists, :board_id)
remove_foreign_key_without_error(:protected_branch_merge_access_levels,
:protected_branch_id)
remove_foreign_key_without_error(:protected_branch_push_access_levels,
:protected_branch_id)
remove_orphaned_rows
add_foreign_keys
# These columns are not indexed yet, meaning a cascading delete would take
# forever.
add_concurrent_index(:project_group_links, :project_id)
add_concurrent_index(:pages_domains, :project_id)
end
def down
TABLES.each do |(source, _, column)|
remove_foreign_key_without_error(source, column)
end
add_concurrent_foreign_key(:boards, :projects, column: :project_id)
add_concurrent_foreign_key(:lists, :labels, column: :label_id)
add_concurrent_foreign_key(:lists, :boards, column: :board_id)
add_concurrent_foreign_key(:protected_branch_merge_access_levels,
:protected_branches,
column: :protected_branch_id)
add_concurrent_foreign_key(:protected_branch_push_access_levels,
:protected_branches,
column: :protected_branch_id)
remove_index_without_error(:project_group_links, :project_id)
remove_index_without_error(:pages_domains, :project_id)
end
def add_foreign_keys
TABLES.each do |(source, target, column)|
add_concurrent_foreign_key(source, target, column: column)
end
end
# Removes orphans from various tables concurrently.
def remove_orphaned_rows
Gitlab::Database.with_connection_pool(CONCURRENCY) do |pool|
queues = queues_for_rows(TABLES)
threads = queues.map do |queue|
Thread.new do
pool.with_connection do |connection|
Thread.current[:foreign_key_connection] = connection
# Disables statement timeouts for the current connection. This is
# necessary as removing of orphaned data might otherwise exceed the
# statement timeout.
disable_statement_timeout
remove_orphans(*queue.pop) until queue.empty?
steal_from_queues(queues - [queue])
end
end
end
threads.each(&:join)
end
end
def steal_from_queues(queues)
loop do
stolen = false
queues.each do |queue|
# Stealing is racy so it's possible a pop might be called on an
# already-empty queue.
begin
remove_orphans(*queue.pop(true))
stolen = true
rescue ThreadError
end
end
break unless stolen
end
end
def remove_orphans(source, target, column)
quoted_source = quote_table_name(source)
quoted_target = quote_table_name(target)
quoted_column = quote_column_name(column)
execute <<-EOF.strip_heredoc
DELETE FROM #{quoted_source}
WHERE NOT EXISTS (
SELECT true
FROM #{quoted_target}
WHERE #{quoted_target}.id = #{quoted_source}.#{quoted_column}
)
AND #{quoted_source}.#{quoted_column} IS NOT NULL
EOF
end
def remove_foreign_key_without_error(table, column)
remove_foreign_key(table, column: column)
rescue ArgumentError
end
def remove_index_without_error(table, column)
remove_concurrent_index(table, column)
rescue ArgumentError
end
def connection
# Rails memoizes connection objects, but this causes them to be shared
# amongst threads; we don't want that.
Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
end
def queues_for_rows(rows)
queues = Array.new(CONCURRENCY) { Queue.new }
slice_size = rows.length / CONCURRENCY
# Divide all the tuples as evenly as possible amongst the queues.
rows.each_slice(slice_size).each_with_index do |slice, index|
bucket = index % CONCURRENCY
slice.each do |row|
queues[bucket] << row
end
end
queues
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CorrectProtectedBranchesForeignKeys < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
remove_foreign_key_without_error(:protected_branch_push_access_levels,
column: :protected_branch_id)
execute <<-EOF
DELETE FROM protected_branch_push_access_levels
WHERE NOT EXISTS (
SELECT true
FROM protected_branches
WHERE protected_branch_push_access_levels.protected_branch_id = protected_branches.id
)
AND protected_branch_id IS NOT NULL
EOF
add_concurrent_foreign_key(:protected_branch_push_access_levels,
:protected_branches,
column: :protected_branch_id)
end
def down
# Previously there was a foreign key without a CASCADING DELETE, so we'll
# just leave the foreign key in place.
end
def remove_foreign_key_without_error(*args)
remove_foreign_key(*args)
rescue ArgumentError
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddForeignKeyForMergeRequestDiffs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
execute <<-EOF
DELETE FROM merge_request_diffs
WHERE NOT EXISTS (
SELECT true
FROM merge_requests
WHERE merge_requests.id = merge_request_diffs.merge_request_id
)
EOF
add_concurrent_foreign_key(:merge_request_diffs,
:merge_requests,
column: :merge_request_id)
end
def down
remove_foreign_key(:merge_request_diffs, column: :merge_request_id)
end
end
...@@ -971,6 +971,7 @@ ...@@ -971,6 +971,7 @@
end end
add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
add_index "pages_domains", ["project_id"], name: "index_pages_domains_on_project_id", using: :btree
create_table "personal_access_tokens", force: :cascade do |t| create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
...@@ -1020,6 +1021,7 @@ ...@@ -1020,6 +1021,7 @@
end end
add_index "project_group_links", ["group_id"], name: "index_project_group_links_on_group_id", using: :btree add_index "project_group_links", ["group_id"], name: "index_project_group_links_on_group_id", using: :btree
add_index "project_group_links", ["project_id"], name: "index_project_group_links_on_project_id", using: :btree
create_table "project_import_data", force: :cascade do |t| create_table "project_import_data", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
...@@ -1528,48 +1530,75 @@ ...@@ -1528,48 +1530,75 @@
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
add_foreign_key "boards", "projects" add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
add_foreign_key "container_repositories", "projects" add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade