Skip to content
代码片段 群组 项目
未验证 提交 8a0a1240 编辑于 作者: Doug Stull's avatar Doug Stull 提交者: GitLab
浏览文件

Add upsert to database review guidelines

- similar to update, destroy, etc, this method should require
  database review
上级 0614fb32
No related branches found
No related tags found
无相关合并请求
# frozen_string_literal: true # frozen_string_literal: true
bulk_database_actions.add_comment_for_bulk_database_action_method_usage helper.all_changed_files.each do |filename|
next unless filename.end_with?('.rb')
next if filename.start_with?('spec/', 'ee/spec/', 'jh/spec/')
bulk_database_actions.add_suggestions_for(filename)
end
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
module Danger module Danger
class BulkDatabaseActions < ::Danger::Plugin class BulkDatabaseActions < ::Danger::Plugin
include Tooling::Danger::BulkDatabaseActions def add_suggestions_for(filename)
Tooling::Danger::BulkDatabaseActions.new(filename, context: self).suggest
end
end end
end end
...@@ -29,7 +29,7 @@ A database review is required for: ...@@ -29,7 +29,7 @@ A database review is required for:
These metrics could have complex queries over large tables. These metrics could have complex queries over large tables.
See the [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) See the [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/)
for implementation details. for implementation details.
- Changes that use [`update`, `delete`, `update_all`, `delete_all` or `destroy_all`](#preparation-when-using-update-delete-update_all-delete_all-or-destroy_all) - Changes that use [`update`, `upsert`, `delete`, `update_all`, `upsert_all`, `delete_all` or `destroy_all`](#preparation-when-using-bulk-update-operations)
methods on an ActiveRecord object. methods on an ActiveRecord object.
A database reviewer is expected to look out for overly complex A database reviewer is expected to look out for overly complex
...@@ -227,9 +227,10 @@ Include in the MR description: ...@@ -227,9 +227,10 @@ Include in the MR description:
- If you're adding a composite index, another index might become redundant, so remove that in the same migration. - If you're adding a composite index, another index might become redundant, so remove that in the same migration.
For example adding `index(column_A, column_B, column_C)` makes the indexes `index(column_A, column_B)` and `index(column_A)` redundant. For example adding `index(column_A, column_B, column_C)` makes the indexes `index(column_A, column_B)` and `index(column_A)` redundant.
#### Preparation when using `update`, `delete`, `update_all`, `delete_all` or `destroy_all` #### Preparation when using bulk update operations
Using these ActiveRecord methods requires extra care because they modify data and can perform poorly, or they Using `update`, `upsert`, `delete`, `update_all`, `upsert_all`, `delete_all` or `destroy_all`
ActiveRecord methods requires extra care because they modify data and can perform poorly, or they
can destroy data if improperly scoped. These methods are also can destroy data if improperly scoped. These methods are also
[incompatible with Common Table Expression (CTE) statements](sql.md#when-to-use-common-table-expressions). [incompatible with Common Table Expression (CTE) statements](sql.md#when-to-use-common-table-expressions).
Danger will comment on a Merge Request Diff when these methods are used. Danger will comment on a Merge Request Diff when these methods are used.
......
...@@ -8,115 +8,87 @@ ...@@ -8,115 +8,87 @@
require_relative '../../../tooling/danger/project_helper' require_relative '../../../tooling/danger/project_helper'
RSpec.describe Tooling::Danger::BulkDatabaseActions, feature_category: :tooling do RSpec.describe Tooling::Danger::BulkDatabaseActions, feature_category: :tooling do
include_context "with dangerfile" include_context 'with dangerfile'
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } let(:fake_danger) { DangerSpecHelper.fake_danger }
let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) } let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
let(:comment_text) { "\n#{described_class::SUGGESTION}" }
let(:mr_url) { 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1' } let(:file_lines) { file_diff.map { |line| line.delete_prefix('+') } }
let(:doc_link) { described_class::DOCUMENTATION_LINK }
let(:comment_text) { "\n#{described_class::COMMENT_TEXT}" }
let(:file_lines) do
file_diff.map { |line| line.delete_prefix('+') }
end
before do before do
allow(bulk_database_actions).to receive(:project_helper).and_return(fake_project_helper) allow(bulk_database_actions).to receive(:project_helper).and_return(fake_project_helper)
allow(bulk_database_actions.project_helper).to receive(:file_lines).and_return(file_lines) allow(bulk_database_actions.project_helper).to receive(:file_lines).and_return(file_lines)
allow(bulk_database_actions.helper).to receive(:added_files).and_return([filename]) allow(bulk_database_actions.helper).to receive(:added_files).and_return([filename])
allow(bulk_database_actions.helper).to receive(:changed_lines).with(filename).and_return(file_diff) allow(bulk_database_actions.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
allow(bulk_database_actions.helper).to receive(:mr_web_url).and_return(mr_url)
bulk_database_actions.define_singleton_method(:add_suggestions_for) do |filename|
Tooling::Danger::BulkDatabaseActions.new(filename, context: self).suggest
end
end end
subject(:bulk_database_actions) { fake_danger.new(helper: fake_helper) } subject(:bulk_database_actions) { fake_danger.new(helper: fake_helper) }
shared_examples 'no Danger comment' do context 'for single line method call' do
it 'does not comment on the bulk update action usage' do let(:file_diff) do
expect(bulk_database_actions).not_to receive(:markdown) <<~DIFF.split("\n")
+ def execute
bulk_database_actions.add_comment_for_bulk_database_action_method_usage + #{code}
+
+ ServiceResponse.success
+ end
DIFF
end end
end
describe '#add_comment_for_bulk_database_action_method_usage' do context 'when file is a non-spec Ruby file' do
context 'for single line method call' do let(:filename) { 'app/services/personal_access_tokens/revoke_token_family_service.rb' }
let(:file_diff) do
[
"+ def execute",
"+ pat_family.active.#{method_call}",
"+",
"+ ServiceResponse.success",
"+ end"
]
end
context 'when file is a non-spec Ruby file' do using RSpec::Parameterized::TableSyntax
let(:filename) { 'app/services/personal_access_tokens/revoke_token_family_service.rb' }
context 'when comment is expected' do
using RSpec::Parameterized::TableSyntax where(:code) do
[
where(:method_call, :expect_comment?) do 'update_all(revoked: true)',
'update_all(revoked: true)' | true 'destroy_all',
'destroy_all' | true 'delete_all',
'delete_all' | true 'update(revoked: true)',
'update(revoked: true)' | true 'delete',
'delete' | true 'upsert',
'update_two_factor' | false 'upsert_all',
'delete_keys(key)' | false 'User.upsert',
'destroy_hook(hook)' | false 'User.last.destroy',
'destroy_all_merged' | false ' .destroy'
'update_all_mirrors' | false ]
end end
with_them do with_them do
it "correctly handles potential bulk database action" do specify do
if expect_comment? expect(bulk_database_actions).to receive(:markdown).with(comment_text.chomp, file: filename, line: 2)
expect(bulk_database_actions).to receive(:markdown).with(comment_text, file: filename, line: 2)
else bulk_database_actions.add_suggestions_for(filename)
expect(bulk_database_actions).not_to receive(:markdown)
end
bulk_database_actions.add_comment_for_bulk_database_action_method_usage
end end
end end
end end
context 'for spec directories' do context 'when no comment is expected' do
let(:method_call) { 'update_all(revoked: true)' } where(:code) do
[
context 'for FOSS spec file' do 'we update bob',
let(:filename) { 'spec/services/personal_access_tokens/revoke_token_family_service_spec.rb' } 'update_two_factor',
'delete_keys(key)',
it_behaves_like 'no Danger comment' 'destroy_hook(hook)',
end 'destroy_all_merged',
'update_all_mirrors'
context 'for EE spec file' do ]
let(:filename) { 'ee/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb' }
it_behaves_like 'no Danger comment'
end end
context 'for JiHu spec file' do with_them do
let(:filename) { 'jh/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb' } specify do
expect(bulk_database_actions).not_to receive(:markdown)
it_behaves_like 'no Danger comment' bulk_database_actions.add_suggestions_for(filename)
end
end end
end end
end end
context 'for strings' do
let(:filename) { 'app/services/personal_access_tokens/revoke_token_family_service.rb' }
let(:file_diff) do
[
'+ expect { subject }.to output(',
'+ "ERROR: Could not update tag"',
'+ ).to_stderr'
]
end
it_behaves_like 'no Danger comment'
end
end end
end end
...@@ -251,6 +251,11 @@ ...@@ -251,6 +251,11 @@
[:backend, :analytics_instrumentation] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb'] [:backend, :analytics_instrumentation] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb']
[:backend, :analytics_instrumentation] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb'] [:backend, :analytics_instrumentation] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb']
[:backend] | '+ count(User.active)' | ['user.rb'] [:backend] | '+ count(User.active)' | ['user.rb']
[:database, :backend] | '+ User.upsert({ name: "blah" })' | ['app/foo/bar.rb']
[:database, :backend] | '+ upsert({ name: "blah" })' | ['app/foo/bar.rb']
[:database, :backend] | '+ .upsert({ name: "blah" })' | ['app/foo/bar.rb']
[:database, :backend] | '+ .delete_all' | ['app/foo/bar.rb']
[:database, :backend] | '+ .destroy_all' | ['app/foo/bar.rb']
[:import_integrate_be, :database] | '+ add_column :integrations, :foo, :text' | ['db/migrate/foo.rb'] [:import_integrate_be, :database] | '+ add_column :integrations, :foo, :text' | ['db/migrate/foo.rb']
[:import_integrate_be, :database] | '+ create_table :zentao_tracker_data do |t|' | ['ee/db/post_migrate/foo.rb'] [:import_integrate_be, :database] | '+ create_table :zentao_tracker_data do |t|' | ['ee/db/post_migrate/foo.rb']
[:import_integrate_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb'] [:import_integrate_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb']
......
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'suggestor' require_relative 'suggestion'
module Tooling module Tooling
module Danger module Danger
module BulkDatabaseActions class BulkDatabaseActions < Suggestion
include ::Tooling::Danger::Suggestor MATCH = %r{\A\+\s+(\S*\.)?((update|upsert|delete|destroy)(_all)?)\b}
REPLACEMENT = nil
DOCUMENTATION_LINK = 'https://docs.gitlab.com/ee/development/database_review.html#preparation-when-using-bulk-update-operations'
BULK_UPDATE_METHODS_REGEX = /\.((update|delete|destroy)(_all)?)\b/ SUGGESTION = <<~MESSAGE_MARKDOWN.freeze
When using `update`, `upsert`, `delete`, `update_all`, `upsert_all`, `delete_all` or `destroy_all`
you must include the full database query and query execution plan in the merge request description,
and request a ~database review.
DOCUMENTATION_LINK = 'https://docs.gitlab.com/ee/development/database_review.html#preparation-when-using-update-delete-update_all-and-destroy_all' This comment can be ignored if the object is not an ActiveRecord class, since no database query would be generated.
COMMENT_TEXT =
"When using `update`, `delete`, `update_all`, `delete_all` or `destroy_all` you must include the full " \
"database query and query execution plan in the merge request description, and request a ~database review. " \
"This comment can be ignored if the object is not an ActiveRecord class, since no database query " \
"would be generated. For more information, see [Database Review documentation](#{DOCUMENTATION_LINK}).".freeze
def add_comment_for_bulk_database_action_method_usage ----
changed_ruby_files.each do |filename|
add_suggestion(
filename: filename,
regex: BULK_UPDATE_METHODS_REGEX,
comment_text: COMMENT_TEXT
)
end
end
def changed_ruby_files For more information, see [Database Review documentation](#{DOCUMENTATION_LINK}).
helper.added_files.select { |f| f.end_with?('.rb') && !f.start_with?('spec/', 'ee/spec/', 'jh/spec/') } MESSAGE_MARKDOWN
end
end end
end end
end end
...@@ -105,6 +105,7 @@ module ProjectHelper ...@@ -105,6 +105,7 @@ module ProjectHelper
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => [:database, :backend], %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => [:database, :backend],
%r{\A((ee|jh)/)?app/finders/} => [:database, :backend], %r{\A((ee|jh)/)?app/finders/} => [:database, :backend],
%r{\Arubocop/cop/migration(/|\.rb)} => :database, %r{\Arubocop/cop/migration(/|\.rb)} => :database,
[%r{\A((ee|jh)/)?(app|lib)/.+\.rb}, %r{\A\+\s+(\w*\.)?(update_all|upsert|upsert_all|delete_all|destroy_all)(\(.*\))?\s*\z}] => [:database, :backend],
%r{\Alib/gitlab/ci/templates} => :ci_template, %r{\Alib/gitlab/ci/templates} => :ci_template,
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册