Commit 6692bccc authored by Adam Hegyi's avatar Adam Hegyi

Allow left-join stage events

This change adds the capability to left-join stage event timestamp
columns.
parent c98721bf
...@@ -42,6 +42,14 @@ module Gitlab ...@@ -42,6 +42,14 @@ module Gitlab
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def include_in(query)
query
.from(Arel::Nodes::Grouping.new(Arel.sql(object_type.all.to_sql)).as(object_type.table_name))
.joins("LEFT JOIN LATERAL (#{subquery.to_sql}) #{join_expression_name} ON TRUE")
end
# rubocop: enable CodeReuse/ActiveRecord
private private
def resource_label_events_table def resource_label_events_table
......
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueClosed do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueClosed do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue, closed_at: Time.current) }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstAddedToBoard do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstAddedToBoard do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue).tap { |i| i.metrics.update!(first_added_to_board_at: Time.current) } }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstAssociatedWithMilestone do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstAssociatedWithMilestone do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue).tap { |i| i.metrics.update!(first_associated_with_milestone_at: Time.current) } }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -8,4 +8,21 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueLabelAdded d ...@@ -8,4 +8,21 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueLabelAdded d
let(:params) { { label: GroupLabel.new(id: label_id) } } let(:params) { { label: GroupLabel.new(id: label_id) } }
let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") } let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") }
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:project) { create(:project) }
let_it_be(:record_with_data) { create(:issue, project: project) }
let_it_be(:record_without_data) { create(:issue) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:user) { project.owner }
let(:params) { { label: label } }
before(:context) do
# adding label via the service so the resource_label_events record is populated
Sidekiq::Worker.skipping_transaction_check do
Issues::UpdateService.new(project: project, current_user: user, params: { label_ids: [label.id] }).execute(record_with_data)
end
end
end
end end
...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueLabelRemoved ...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueLabelRemoved
let(:params) { { label: GroupLabel.new(id: label_id) } } let(:params) { { label: GroupLabel.new(id: label_id) } }
let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") } let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") }
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:project) { create(:project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:record_with_data) { create(:labeled_issue, project: project, labels: [label]) }
let_it_be(:record_without_data) { create(:issue) }
let_it_be(:user) { project.owner }
let(:params) { { label: label } }
before(:context) do
Sidekiq::Worker.skipping_transaction_check do
Issues::UpdateService.new(project: project, current_user: user, params: { label_ids: [] }).execute(record_with_data)
end
end
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestClosed do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestClosed do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(latest_closed_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstCommitAt do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstCommitAt do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(first_commit_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLabel ...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLabel
let(:params) { { label: GroupLabel.new(id: label_id) } } let(:params) { { label: GroupLabel.new(id: label_id) } }
let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") } let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") }
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:record_with_data) { create(:merge_request, :unique_branches, source_project: project) }
let_it_be(:record_without_data) { create(:merge_request) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:user) { project.owner }
let(:params) { { label: label } }
before(:context) do
Sidekiq::Worker.skipping_transaction_check do
MergeRequests::UpdateService.new(project: project, current_user: user, params: { label_ids: [label.id] }).execute(record_with_data)
end
end
end
end end
...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLabel ...@@ -8,4 +8,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLabel
let(:params) { { label: GroupLabel.new(id: label_id) } } let(:params) { { label: GroupLabel.new(id: label_id) } }
let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") } let(:expected_hash_code) { Digest::SHA256.hexdigest("#{instance.class.identifier}-#{label_id}") }
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:record_with_data) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:record_without_data) { create(:merge_request) }
let_it_be(:user) { project.owner }
let(:params) { { label: label } }
before(:context) do
Sidekiq::Worker.skipping_transaction_check do
MergeRequests::UpdateService.new(project: project, current_user: user, params: { label_ids: [] }).execute(record_with_data)
end
end
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastEdited do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastEdited do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request, last_edited_at: Time.current) }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -53,6 +53,10 @@ module Gitlab ...@@ -53,6 +53,10 @@ module Gitlab
.on(mr_metrics_table[:merge_request_id].eq(mr_table[:id])) .on(mr_metrics_table[:merge_request_id].eq(mr_table[:id]))
.join_sources .join_sources
end end
def include_in(query)
query.left_joins(merge_requests_closing_issues: { issue: [:metrics] }, metrics: [])
end
end end
end end
end end
......
...@@ -26,6 +26,10 @@ module Gitlab ...@@ -26,6 +26,10 @@ module Gitlab
query.joins(merge_requests_closing_issues: { merge_request: [:metrics] }).where(mr_metrics_table[:first_deployed_to_production_at].gteq(mr_table[:created_at])) query.joins(merge_requests_closing_issues: { merge_request: [:metrics] }).where(mr_metrics_table[:first_deployed_to_production_at].gteq(mr_table[:created_at]))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def include_in(query)
query.left_joins(merge_requests_closing_issues: { merge_request: [:metrics] })
end
end end
end end
end end
......
...@@ -20,6 +20,10 @@ module Gitlab ...@@ -20,6 +20,10 @@ module Gitlab
def column_list def column_list
[timestamp_projection] [timestamp_projection]
end end
def include_in(query)
super.left_joins(:metrics)
end
end end
end end
end end
......
...@@ -61,6 +61,10 @@ module Gitlab ...@@ -61,6 +61,10 @@ module Gitlab
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def include_in(query)
query
end
def self.label_based? def self.label_based?
false false
end end
......
...@@ -19,4 +19,16 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::CodeStageStart do ...@@ -19,4 +19,16 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::CodeStageStart do
expect(records).to eq([merge_request]) expect(records).to eq([merge_request])
expect(records).not_to include(other_merge_request) expect(records).not_to include(other_merge_request)
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) do
mr_closing_issue = FactoryBot.create(:merge_requests_closing_issues)
issue = mr_closing_issue.issue
issue.metrics.update!(first_mentioned_in_commit_at: Time.current)
mr_closing_issue.merge_request
end
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,8 @@ require 'spec_helper' ...@@ -4,4 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue) }
end
end end
...@@ -4,4 +4,16 @@ require 'spec_helper' ...@@ -4,4 +4,16 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueDeployedToProduction do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueDeployedToProduction do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) do
mr_closing_issue = FactoryBot.create(:merge_requests_closing_issues)
mr = mr_closing_issue.merge_request
mr.metrics.update!(first_deployed_to_production_at: Time.current)
mr_closing_issue.issue
end
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue).tap { |i| i.metrics.update!(first_mentioned_in_commit_at: Time.current) } }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue).tap { |i| i.metrics.update!(first_added_to_board_at: Time.current) } }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -4,4 +4,8 @@ require 'spec_helper' ...@@ -4,4 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstDeployedToProduction do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstDeployedToProduction do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(first_deployed_to_production_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildFinished do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildFinished do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(latest_build_finished_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildStarted do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildStarted do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(latest_build_started_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -4,4 +4,9 @@ require 'spec_helper' ...@@ -4,4 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged do RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged do
it_behaves_like 'value stream analytics event' it_behaves_like 'value stream analytics event'
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:merge_request).tap { |mr| mr.metrics.update!(merged_at: Time.current) } }
let_it_be(:record_without_data) { create(:merge_request) }
end
end end
...@@ -21,4 +21,9 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::PlanStageStart do ...@@ -21,4 +21,9 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::PlanStageStart do
expect(records).to match_array([issue1, issue2]) expect(records).to match_array([issue1, issue2])
expect(records).not_to include(issue_without_metrics) expect(records).not_to include(issue_without_metrics)
end end
it_behaves_like 'LEFT JOIN-able value stream analytics event' do
let_it_be(:record_with_data) { create(:issue).tap { |i| i.metrics.update!(first_added_to_board_at: Time.current) } }
let_it_be(:record_without_data) { create(:issue) }
end
end end
...@@ -33,3 +33,38 @@ RSpec.shared_examples_for 'value stream analytics event' do ...@@ -33,3 +33,38 @@ RSpec.shared_examples_for 'value stream analytics event' do
end end
end end
end end
RSpec.shared_examples_for 'LEFT JOIN-able value stream analytics event' do
let(:params) { {} }
let(:instance) { described_class.new(params) }
let(:record_with_data) { nil }
let(:record_without_data) { nil }
let(:scope) { instance.object_type.all }
let(:records) do
scope_with_left_join = instance.include_in(scope)
scope_with_left_join.select(scope.model.arel_table[:id], instance.timestamp_projection.as('timestamp_column_data')).to_a
end
it 'can use the event as LEFT JOIN' do
expected_record_count = record_without_data.nil? ? 1 : 2
expect(records.count).to eq(expected_record_count)
end
context 'when looking at the record with data' do
subject(:record) { records.to_a.find { |r| r.id == record_with_data.id } }
it 'contains the timestamp expression' do
expect(record.timestamp_column_data).not_to eq(nil)
end
end
context 'when looking at the record without data' do
subject(:record) { records.to_a.find { |r| r.id == record_without_data.id } }
it 'returns nil for the timestamp expression' do
expect(record.timestamp_column_data).to eq(nil) if record_without_data
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