Commit 2c913614 authored by Dallas Reedy's avatar Dallas Reedy

Add new ExperimentSubject model & DB migrations

Adds a new model & database table for ExperimentSubject. An
ExperimentSubject can be a User, Group, or Project (or any combination
of those three). This change prepares the way for us to run a much
broader range of experiments.
parent c846230e
......@@ -2,6 +2,7 @@
class Experiment < ApplicationRecord
has_many :experiment_users
has_many :experiment_subjects
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
......
# frozen_string_literal: true
class ExperimentSubject < ApplicationRecord
include ::Gitlab::Experimentation::GroupTypes
belongs_to :experiment
belongs_to :user
belongs_to :group
belongs_to :project
validates :experiment, presence: true
validates :variant, presence: true
validate :must_have_at_least_one_subject
enum variant: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 }
private
def must_have_at_least_one_subject
if [user, group, project].all?(&:blank?)
errors.add(:base, s_("ExperimentSubject|Must have at least one of User, Group, or Project."))
end
end
end
---
title: Create a new `ExperimentSubject` model, associated to the `Experiment` model, and related database migrations.
merge_request: 47042
author:
type: added
# frozen_string_literal: true
class CreateExperimentSubjects < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
create_table :experiment_subjects do |t|
t.references :experiment, index: true, foreign_key: { on_delete: :cascade }, null: false
t.integer :variant, limit: 2, null: false, default: 0
t.timestamps_with_timezone null: false
end
add_reference :experiment_subjects, :user, index: true, foreign_key: { on_delete: :cascade }
add_reference :experiment_subjects, :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }
add_reference :experiment_subjects, :project, index: true, foreign_key: { on_delete: :cascade }
# Require at least one of user_id, group_id, or project_id to be NOT NULL
execute <<-SQL
ALTER TABLE experiment_subjects ADD CONSTRAINT chk_at_least_one_subject CHECK (NOT ROW(user_id, group_id, project_id) IS NULL);
SQL
end
def down
drop_table :experiment_subjects
end
end
9fba60d8805915fcf6af7812e2c752007ac17bb92c8a02c942c0c790d2997441
\ No newline at end of file
......@@ -12138,6 +12138,27 @@ CREATE SEQUENCE evidences_id_seq
ALTER SEQUENCE evidences_id_seq OWNED BY evidences.id;
CREATE TABLE experiment_subjects (
id bigint NOT NULL,
experiment_id bigint NOT NULL,
variant smallint DEFAULT 0 NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
user_id bigint,
group_id bigint,
project_id bigint,
CONSTRAINT chk_at_least_one_subject CHECK ((NOT (ROW(user_id, group_id, project_id) IS NULL)))
);
CREATE SEQUENCE experiment_subjects_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE experiment_subjects_id_seq OWNED BY experiment_subjects.id;
CREATE TABLE experiment_users (
id bigint NOT NULL,
experiment_id bigint NOT NULL,
......@@ -18147,6 +18168,8 @@ ALTER TABLE ONLY events ALTER COLUMN id SET DEFAULT nextval('events_id_seq'::reg
ALTER TABLE ONLY evidences ALTER COLUMN id SET DEFAULT nextval('evidences_id_seq'::regclass);
ALTER TABLE ONLY experiment_subjects ALTER COLUMN id SET DEFAULT nextval('experiment_subjects_id_seq'::regclass);
ALTER TABLE ONLY experiment_users ALTER COLUMN id SET DEFAULT nextval('experiment_users_id_seq'::regclass);
ALTER TABLE ONLY experiments ALTER COLUMN id SET DEFAULT nextval('experiments_id_seq'::regclass);
......@@ -19309,6 +19332,9 @@ ALTER TABLE ONLY events
ALTER TABLE ONLY evidences
ADD CONSTRAINT evidences_pkey PRIMARY KEY (id);
ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT experiment_subjects_pkey PRIMARY KEY (id);
ALTER TABLE ONLY experiment_users
ADD CONSTRAINT experiment_users_pkey PRIMARY KEY (id);
......@@ -21181,6 +21207,14 @@ CREATE UNIQUE INDEX index_events_on_target_type_and_target_id_and_fingerprint ON
CREATE INDEX index_evidences_on_release_id ON evidences USING btree (release_id);
CREATE INDEX index_experiment_subjects_on_experiment_id ON experiment_subjects USING btree (experiment_id);
CREATE INDEX index_experiment_subjects_on_group_id ON experiment_subjects USING btree (group_id);
CREATE INDEX index_experiment_subjects_on_project_id ON experiment_subjects USING btree (project_id);
CREATE INDEX index_experiment_subjects_on_user_id ON experiment_subjects USING btree (user_id);
CREATE INDEX index_experiment_users_on_experiment_id ON experiment_users USING btree (experiment_id);
CREATE INDEX index_experiment_users_on_user_id ON experiment_users USING btree (user_id);
......@@ -24454,6 +24488,9 @@ ALTER TABLE ONLY ci_runner_namespaces
ALTER TABLE ONLY software_license_policies
ADD CONSTRAINT fk_rails_87b2247ce5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT fk_rails_88489af1b1 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY protected_environment_deploy_access_levels
ADD CONSTRAINT fk_rails_898a13b650 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE;
......@@ -24826,6 +24863,9 @@ ALTER TABLE ONLY operations_strategies_user_lists
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_rails_ccc0840427 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE;
ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT fk_rails_ccc28f8ceb FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_milestone_events
ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
......@@ -24892,6 +24932,9 @@ ALTER TABLE ONLY vulnerability_feedback
ALTER TABLE ONLY analytics_cycle_analytics_group_stages
ADD CONSTRAINT fk_rails_dfb37c880d FOREIGN KEY (end_event_label_id) REFERENCES labels(id) ON DELETE CASCADE;
ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT fk_rails_dfc3e211d4 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY label_priorities
ADD CONSTRAINT fk_rails_e161058b0f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
......@@ -24958,6 +25001,9 @@ ALTER TABLE ONLY snippet_statistics
ALTER TABLE ONLY project_security_settings
ADD CONSTRAINT fk_rails_ed4abe1338 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT fk_rails_ede5754774 FOREIGN KEY (experiment_id) REFERENCES experiments(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_daily_build_group_report_results
ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
......
......@@ -11307,6 +11307,9 @@ msgstr ""
msgid "Experienced"
msgstr ""
msgid "ExperimentSubject|Must have at least one of User, Group, or Project."
msgstr ""
msgid "Expiration"
msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
factory :experiment_subject do
experiment
user
variant { :control }
end
end
......@@ -7,6 +7,7 @@ RSpec.describe Experiment do
describe 'associations' do
it { is_expected.to have_many(:experiment_users) }
it { is_expected.to have_many(:experiment_subjects) }
end
describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ExperimentSubject, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:experiment) }
end
describe 'validate must_have_at_least_one_subject' do
let(:experiment_subject) { build(:experiment_subject, user: nil, group: nil, project: nil) }
it 'fails if user, group, & project are blank' do
expect(experiment_subject).not_to be_valid
expect(experiment_subject.errors[:base]).to include("Must have at least one of User, Group, or Project.")
end
it 'passes when user is present' do
experiment_subject.user = build(:user)
expect(experiment_subject).to be_valid
end
it 'passes when group is present' do
experiment_subject.group = build(:group)
expect(experiment_subject).to be_valid
end
it 'passes when project is present' do
experiment_subject.project = build(:project)
expect(experiment_subject).to be_valid
end
it 'passes when multiple subjects are present' do
experiment_subject.user = build(:user)
experiment_subject.group = build(:group)
experiment_subject.project = build(:project)
expect(experiment_subject).to be_valid
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