Commit b7abba0c authored by Yorick Peterse's avatar Yorick Peterse

Revamp trending projects query

This changes the query to use a COUNT nested in an INNER JOIN, instead
of a COUNT plus a GROUP BY. There are two reasons for this:

1. Using a COUNT in an INNER JOIN can be quite a bit faster.
2. The use of a GROUP BY means that method calls such as "any?"
   (and everything else that calls "count") operate on a Hash that
   counts the amount of notes on a per project basis, instead of just
   counting the total amount of projects.

The query has been moved into Project.trending as its logic is simple
enough. As a result of this testing the TrendingProjectsFinder class
simply involves testing if the right methods are called, removing the
need for setting up database records.
parent d15eec64
class TrendingProjectsFinder class TrendingProjectsFinder
def execute(current_user, start_date = nil) def execute(current_user, start_date = 1.month.ago)
start_date ||= Date.today - 1.month projects_for(current_user).trending(start_date)
projects = projects_for(current_user)
# Determine trending projects based on comments count
# for period of time - ex. month
projects.joins(:notes).where('notes.created_at >= ?', start_date).
group("projects.id").reorder("count(notes.id) DESC")
end end
private private
......
...@@ -260,6 +260,20 @@ class Project < ActiveRecord::Base ...@@ -260,6 +260,20 @@ class Project < ActiveRecord::Base
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
%r{(?<project>#{name_pattern}/#{name_pattern})} %r{(?<project>#{name_pattern}/#{name_pattern})}
end end
def trending(since = 1.month.ago)
# By counting in the JOIN we don't expose the GROUP BY to the outer query.
# This means that calls such as "any?" and "count" just return a number of
# the total count, instead of the counts grouped per project as a Hash.
join_body = "INNER JOIN (
SELECT project_id, COUNT(*) AS amount
FROM notes
WHERE created_at >= #{sanitize(since)}
GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id"
joins(join_body).reorder('join_note_counts.amount DESC')
end
end end
def team def team
......
require 'spec_helper' require 'spec_helper'
describe TrendingProjectsFinder do describe TrendingProjectsFinder do
let(:user) { create(:user) } let(:user) { build(:user) }
let(:group) { create(:group) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :public, group: group) }
before do
2.times do
create(:note_on_commit, project: project1)
end
create(:note_on_commit, project: project2)
end
describe '#execute' do describe '#execute' do
describe 'without an explicit start date' do describe 'without an explicit start date' do
subject { described_class.new.execute(user).to_a } subject { described_class.new }
it 'sorts Projects by the amount of notes in descending order' do it 'returns the trending projects' do
expect(subject).to eq([project1, project2]) relation = double(:ar_relation)
allow(subject).to receive(:projects_for)
.with(user)
.and_return(relation)
allow(relation).to receive(:trending)
.with(an_instance_of(ActiveSupport::TimeWithZone))
end end
end end
describe 'with an explicit start date' do describe 'with an explicit start date' do
let(:date) { 2.months.ago } let(:date) { 2.months.ago }
subject { described_class.new.execute(user, date).to_a } subject { described_class.new }
before do it 'returns the trending projects' do
2.times do relation = double(:ar_relation)
create(:note_on_commit, project: project2, created_at: date)
end allow(subject).to receive(:projects_for)
end .with(user)
.and_return(relation)
it 'sorts Projects by the amount of notes in descending order' do allow(relation).to receive(:trending)
expect(subject).to eq([project2, project1]) .with(date)
end end
end end
end end
......
...@@ -423,4 +423,42 @@ describe Project do ...@@ -423,4 +423,42 @@ describe Project do
it { expect(project.gitlab_ci?).to be_truthy } it { expect(project.gitlab_ci?).to be_truthy }
it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } it { expect(project.gitlab_ci_project).to be_a(Ci::Project) }
end end
describe '.trending' do
let(:group) { create(:group) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :public, group: group) }
before do
2.times do
create(:note_on_commit, project: project1)
end
create(:note_on_commit, project: project2)
end
describe 'without an explicit start date' do
subject { described_class.trending.to_a }
it 'sorts Projects by the amount of notes in descending order' do
expect(subject).to eq([project1, project2])
end
end
describe 'with an explicit start date' do
let(:date) { 2.months.ago }
subject { described_class.trending(date).to_a }
before do
2.times do
create(:note_on_commit, project: project2, created_at: date)
end
end
it 'sorts Projects by the amount of notes in descending order' do
expect(subject).to eq([project2, project1])
end
end
end
end end
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