Commit 109771dd authored by Imre Farkas's avatar Imre Farkas

Simplify ancestor queries

parent 01b3c750
...@@ -67,7 +67,9 @@ module Namespaces ...@@ -67,7 +67,9 @@ module Namespaces
def ancestors(hierarchy_order: nil) def ancestors(hierarchy_order: nil)
return super() unless use_traversal_ids? return super() unless use_traversal_ids?
lineage(bottom: latest_parent_id, hierarchy_order: hierarchy_order) return self.class.none if parent_id.blank?
lineage(bottom: parent, hierarchy_order: hierarchy_order)
end end
private private
...@@ -99,14 +101,14 @@ module Namespaces ...@@ -99,14 +101,14 @@ module Namespaces
end end
if bottom if bottom
skope = skope.traversal_ids_contained_by(latest_traversal_ids(bottom)) skope = skope.traversal_ids_contained_by(sql_array(bottom.traversal_ids))
end end
# The original `with_depth` attribute in ObjectHierarchy increments as you # The original `with_depth` attribute in ObjectHierarchy increments as you
# walk away from the "base" namespace. This direction changes depending on # walk away from the "base" namespace. This direction changes depending on
# if you are walking up the ancestors or down the descendants. # if you are walking up the ancestors or down the descendants.
if hierarchy_order if hierarchy_order
depth_sql = "ABS(array_length((#{latest_traversal_ids.to_sql}), 1) - array_length(traversal_ids, 1))" depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth") skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
.order(depth: hierarchy_order) .order(depth: hierarchy_order)
end end
...@@ -114,26 +116,8 @@ module Namespaces ...@@ -114,26 +116,8 @@ module Namespaces
skope skope
end end
# traversal_ids are a cached value. def sql_array(ids)
# "{#{ids.join(',')}}"
# The traversal_ids value in a loaded object can become stale when compared
# to the database value. For example, if you load a hierarchy and then move
# a group, any previously loaded descendant objects will have out of date
# traversal_ids.
#
# To solve this problem, we never depend on the object's traversal_ids
# value. We always query the database first with a sub-select for the
# latest traversal_ids.
#
# Note that ActiveRecord will cache query results. You can avoid this by
# using `Model.uncached { ... }`
def latest_traversal_ids(namespace = self)
without_sti_condition.where('id = (?)', namespace)
.select('traversal_ids as latest_traversal_ids')
end
def latest_parent_id(namespace = self)
without_sti_condition.where(id: namespace).select(:parent_id)
end end
end end
end end
......
...@@ -427,6 +427,42 @@ RSpec.describe Group do ...@@ -427,6 +427,42 @@ RSpec.describe Group do
end end
end end
context 'traversal queries' do
let_it_be(:group, reload: true) { create(:group, :nested) }
context 'recursive' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like 'namespace traversal'
describe '#self_and_descendants' do
it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
end
describe '#ancestors' do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
end
context 'linear' do
it_behaves_like 'namespace traversal'
describe '#self_and_descendants' do
it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
end
describe '#ancestors' do
it { expect(group.ancestors.to_sql).to include 'traversal_ids <@' }
end
it 'hierarchy order' do
expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC'
end
end
end
describe '.without_integration' do describe '.without_integration' do
let(:another_group) { create(:group) } let(:another_group) { create(:group) }
let(:instance_integration) { build(:jira_service, :instance) } let(:instance_integration) { build(:jira_service, :instance) }
......
...@@ -936,42 +936,6 @@ RSpec.describe Namespace do ...@@ -936,42 +936,6 @@ RSpec.describe Namespace do
end end
end end
context 'traversal queries' do
let_it_be(:namespace, reload: true) { create(:namespace) }
context 'recursive' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like 'namespace traversal'
describe '#self_and_descendants' do
it { expect(namespace.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
end
describe '#ancestors' do
it { expect(namespace.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
end
context 'linear' do
it_behaves_like 'namespace traversal'
describe '#self_and_descendants' do
it { expect(namespace.self_and_descendants.to_sql).to include 'traversal_ids @>' }
end
describe '#ancestors' do
it { expect(namespace.ancestors.to_sql).to include 'traversal_ids <@' }
it 'hierarchy order' do
expect(namespace.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC'
end
end
end
end
describe '#users_with_descendants' do describe '#users_with_descendants' do
let(:user_a) { create(:user) } let(:user_a) { create(:user) }
let(:user_b) { create(:user) } let(:user_b) { create(:user) }
......
...@@ -240,6 +240,7 @@ RSpec.describe Groups::TransferService do ...@@ -240,6 +240,7 @@ RSpec.describe Groups::TransferService do
end end
context 'when the group is allowed to be transferred' do context 'when the group is allowed to be transferred' do
let_it_be(:new_parent_group, reload: true) { create(:group, :public) }
let_it_be(:new_parent_group_integration) { create(:slack_service, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') } let_it_be(:new_parent_group_integration) { create(:slack_service, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') }
before do before do
...@@ -273,11 +274,10 @@ RSpec.describe Groups::TransferService do ...@@ -273,11 +274,10 @@ RSpec.describe Groups::TransferService do
end end
context 'with a group integration' do context 'with a group integration' do
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
let(:new_created_integration) { Service.find_by(group: group) } let(:new_created_integration) { Service.find_by(group: group) }
context 'with an inherited integration' do context 'with an inherited integration' do
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) } let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) }
it 'replaces inherited integrations', :aggregate_failures do it 'replaces inherited integrations', :aggregate_failures do
...@@ -603,6 +603,7 @@ RSpec.describe Groups::TransferService do ...@@ -603,6 +603,7 @@ RSpec.describe Groups::TransferService do
create(:group_member, :owner, group: new_parent_group, user: user) create(:group_member, :owner, group: new_parent_group, user: user)
create(:group, :private, parent: group, require_two_factor_authentication: true) create(:group, :private, parent: group, require_two_factor_authentication: true)
group.update!(require_two_factor_authentication: true) group.update!(require_two_factor_authentication: true)
new_parent_group.reload # make sure traversal_ids are reloaded
end end
it 'does not update group two factor authentication setting' do it 'does not update group two factor authentication setting' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment