Commit 302866a9 authored by Mario de la Ossa's avatar Mario de la Ossa

Iterations - constraint date ranges do not overlap

Adds btree_gist postgresql extension
Makes it so that Iterations inside the same group/project cannot have
overlapping date ranges.
parent 828aef3a
---
title: Add btree_gist PGSQL extension and add DB constraints for Iteration date ranges
merge_request: 32335
author:
type: added
# frozen_string_literal: true
class EnableBtreeGistExtension < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
execute 'CREATE EXTENSION IF NOT EXISTS btree_gist'
end
def down
execute 'DROP EXTENSION IF EXISTS btree_gist'
end
end
# frozen_string_literal: true
class IterationDateRangeConstraint < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
execute <<~SQL
ALTER TABLE sprints
ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint
EXCLUDE USING gist
( project_id WITH =,
daterange(start_date, due_date, '[]') WITH &&
)
WHERE (project_id IS NOT NULL)
SQL
execute <<~SQL
ALTER TABLE sprints
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint
EXCLUDE USING gist
( group_id WITH =,
daterange(start_date, due_date, '[]') WITH &&
)
WHERE (group_id IS NOT NULL)
SQL
end
def down
execute <<~SQL
ALTER TABLE sprints
DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_project_id_constraint
SQL
execute <<~SQL
ALTER TABLE sprints
DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_group_id_constraint
SQL
end
end
...@@ -2,6 +2,8 @@ SET search_path=public; ...@@ -2,6 +2,8 @@ SET search_path=public;
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE TABLE public.abuse_reports ( CREATE TABLE public.abuse_reports (
...@@ -8385,6 +8387,12 @@ ALTER TABLE ONLY public.issue_user_mentions ...@@ -8385,6 +8387,12 @@ ALTER TABLE ONLY public.issue_user_mentions
ALTER TABLE ONLY public.issues ALTER TABLE ONLY public.issues
ADD CONSTRAINT issues_pkey PRIMARY KEY (id); ADD CONSTRAINT issues_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.sprints
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint EXCLUDE USING gist (group_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL));
ALTER TABLE ONLY public.sprints
ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint EXCLUDE USING gist (project_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((project_id IS NOT NULL));
ALTER TABLE ONLY public.jira_connect_installations ALTER TABLE ONLY public.jira_connect_installations
ADD CONSTRAINT jira_connect_installations_pkey PRIMARY KEY (id); ADD CONSTRAINT jira_connect_installations_pkey PRIMARY KEY (id);
...@@ -13866,6 +13874,8 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13866,6 +13874,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200514000009 20200514000009
20200514000132 20200514000132
20200514000340 20200514000340
20200515152649
20200515153633
20200515155620 20200515155620
\. \.
...@@ -50,7 +50,10 @@ describe Iteration do ...@@ -50,7 +50,10 @@ describe Iteration do
end end
context 'when dates overlap' do context 'when dates overlap' do
context 'same group' do let(:start_date) { 5.days.from_now }
let(:due_date) { 6.days.from_now }
shared_examples_for 'overlapping dates' do
context 'when start_date is in range' do context 'when start_date is in range' do
let(:start_date) { 5.days.from_now } let(:start_date) { 5.days.from_now }
let(:due_date) { 3.weeks.from_now } let(:due_date) { 3.weeks.from_now }
...@@ -59,6 +62,11 @@ describe Iteration do ...@@ -59,6 +62,11 @@ describe Iteration do
expect(subject).not_to be_valid expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end end
it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end end
context 'when end_date is in range' do context 'when end_date is in range' do
...@@ -69,25 +77,84 @@ describe Iteration do ...@@ -69,25 +77,84 @@ describe Iteration do
expect(subject).not_to be_valid expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end end
it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end end
context 'when both overlap' do context 'when both overlap' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 6.days.from_now }
it 'is not valid' do it 'is not valid' do
expect(subject).not_to be_valid expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end end
it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end end
end end
context 'different group' do context 'group' do
let(:start_date) { 5.days.from_now } it_behaves_like 'overlapping dates' do
let(:due_date) { 6.days.from_now } let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' }
let(:group) { create(:group) } end
context 'different group' do
let(:group) { create(:group) }
it { is_expected.to be_valid }
it 'does not trigger exclusion constraints' do
expect { subject.save }.not_to raise_exception
end
end
context 'in a project' do
let(:project) { create(:project) }
subject { build(:iteration, project: project, start_date: start_date, due_date: due_date) }
it { is_expected.to be_valid }
it { is_expected.to be_valid } it 'does not trigger exclusion constraints' do
expect { subject.save }.not_to raise_exception
end
end
end
context 'project' do
let_it_be(:existing_iteration) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
subject { build(:iteration, project: project, start_date: start_date, due_date: due_date) }
it_behaves_like 'overlapping dates' do
let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' }
end
context 'different project' do
let(:project) { create(:project) }
it { is_expected.to be_valid }
it 'does not trigger exclusion constraints' do
expect { subject.save }.not_to raise_exception
end
end
context 'in a group' do
let(:group) { create(:group) }
subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
it { is_expected.to be_valid }
it 'does not trigger exclusion constraints' do
expect { subject.save }.not_to raise_exception
end
end
end 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