diff --git a/docs/concurrentRevisionHandling.md b/docs/concurrentRevisionHandling.md index 4839bce7c5..162820d113 100644 --- a/docs/concurrentRevisionHandling.md +++ b/docs/concurrentRevisionHandling.md @@ -16,12 +16,13 @@ An Amendment is opened on a project, and left open while it is being negotiated. A solution that would allow us to handle concurrency without user input on conflict resolution was needed. To achieve this, the approach taken is comparable to a git rebase. When committing and pending are in conflict, the changes made in pending are applied on top of the committing form change, as if the committing `form_change` were the original parent of the pending `form_change`. While users commit on a `project_revision` level, the change propogates down to the `form_change` level, so when we're talking about this here it is at the `form_change` granularity, and the heart it takes place in the function `cif.commit_form_change_internal`. One of the ways our various forms can be categorized would be: + - forms a project can have at most one of (`funding_parameter_EP`, `funding_parameter_IA`, `emission_intensity`, `project_summary_report`) - 'project_contact' are either primary or secondary, and have a `contactIndex` - 'project_manager' are categorized by `projectManagerLabelId` - 'reporting_requirement' have a `reportingRequirementIndex` based on the `json_schema_name` -Form changes can have an operation of `create`, `update`, or `archive`, each of which need to be handled for all of the above categories. This results in several unique cases, which have been explained case-by-case using in-line in the `commit_form_change_internal` where they have more context. +Form changes can have an operation of `create`, `update`, or `archive`, each of which need to be handled for all of the above categories. This results in several unique cases, which have been explained case-by-case using in-line in the `commit_form_change_internal` where they have more context. After each of the following cases, the `previous_form_change_id` of the pending `form_change` is set to be the id of the committing `form_change`, which leaves every form change with a `previous_form_change_id` of the **last commit** corresponding `form_change`, while preserving the option of a full history by maintaining accurate `created_at`, `updated_at`, and `archived_at` values for all `form_change`. diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 1a233cfdeb..c70eada702 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -134,7 +134,7 @@ begin project_revision_id => pending_project_revision_id, json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"reportingRequirementIndex": %s}', - (select max((new_form_data ->> 'reportingRequirementIndex')::int) from cif.form_change + (select max((new_form_data ->> 'reportingRequirementIndex')::int) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name = fc.json_schema_name and new_form_data ->> 'reportType' = fc.new_form_data ->> 'reportType' @@ -163,7 +163,7 @@ begin project_revision_id => pending_project_revision_id, json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"reportingRequirementIndex": %s}', - (select max((new_form_data ->> 'reportingRequirementIndex')::int) from cif.form_change + (select max((new_form_data ->> 'reportingRequirementIndex')::int) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name = fc.json_schema_name ) + 1)::jsonb diff --git a/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql b/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql deleted file mode 100644 index ebe8867e20..0000000000 --- a/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql +++ /dev/null @@ -1,49 +0,0 @@ -begin; - -select plan(2); - -truncate table cif.project, cif.operator, cif.contact, cif.attachment restart identity cascade; -insert into cif.operator(legal_name) values ('test operator'); -insert into cif.contact(given_name, family_name, email) values ('John', 'Test', 'foo@abc.com'); -insert into cif.attachment (description, file_name, file_type, file_size) - values ('description1', 'file_name1', 'file_type1', 100); - -select cif.create_project(1); -- id = 1 -update cif.form_change set new_form_data='{ - "projectName": "name", - "summary": "original (incorrect at point of test)", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=1 - and form_data_table_name='project'; -select cif.commit_project_revision(1); - -select cif.create_project_revision(1, 'Amendment'); -- id = 2 -select cif.create_project_revision(1, 'General Revision'); -- id = 3 - -select cif.discard_project_attachment_form_change((select id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment')); -select cif.commit_project_revision(3); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - 0::bigint, - 'When the committing form change is discarding a project attachment, the pending fc is deleted.' -); - --- Commit the pending ammednment - -select lives_ok ( - $$ - select cif.commit_project_revision(2) - $$, - 'Committing the pending project_revision does not throw an error' -); - - - -select finish(); - -rollback; diff --git a/schema/test/unit/concurrent_revisions/discards_test.sql b/schema/test/unit/concurrent_revisions/discards_test.sql new file mode 100644 index 0000000000..3991ddf09e --- /dev/null +++ b/schema/test/unit/concurrent_revisions/discards_test.sql @@ -0,0 +1,165 @@ +begin; + +select plan(7); + + +truncate table cif.project, cif.operator, cif.contact, cif.attachment restart identity cascade; +insert into cif.operator(legal_name) values ('test operator'); +insert into cif.contact(given_name, family_name, email) values ('John', 'Test', 'foo@abc.com'), ('Sandy', 'Olson', 'bar@abc.com'); +insert into cif.attachment (description, file_name, file_type, file_size) + values ('description1', 'file_name1', 'file_type1', 100), ('description2', 'file_name2', 'file_type1', 100); +insert into cif.cif_user(id, session_sub, given_name, family_name) + overriding system value + values (1, '11111111-1111-1111-1111-111111111111', 'Jan','Jansen'), + (2, '22222222-2222-2222-2222-222222222222', 'Max','Mustermann'), + (3, '33333333-3333-3333-3333-333333333333', 'Eva', 'Nováková'); + +-- Create a project to update. +select cif.create_project(1); -- id = 1 +update cif.form_change set new_form_data='{ + "projectName": "name", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=1 + and form_data_table_name='project'; + +select cif.add_contact_to_revision(1, 1, 1); +select cif.add_contact_to_revision(1, 2, 2); +select cif.add_project_attachment_to_revision(1,1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 1 + )::jsonb, + null, + 1 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 1 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 1 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 1 +); +select cif.commit_project_revision(1); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 + +update cif.form_change set operation = 'archive' + where project_revision_id=3 + and json_schema_name='project_contact' + and new_form_data ->> 'contactIndex' = '2'; + +update cif.form_change set operation = 'archive' + where project_revision_id=3 + and json_schema_name='reporting_requirement' + and new_form_data ->> 'reportType' = 'Quarterly'; + +update cif.form_change set operation = 'archive' + where project_revision_id=3 + and json_schema_name='project_manager'; + +update cif.form_change set operation = 'archive' + where project_revision_id=3 + and json_schema_name='milestone'; + +select cif.discard_funding_parameter_form_change(3); + +select cif.discard_project_attachment_form_change( + (select id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment') +); + +select cif.commit_project_revision(3); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact'), + 1::bigint, + 'When the committing form change archives a project contact, the corresponding form change in the pending revision on that project is deleted' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'reporting_requirement' and new_form_data ->> 'reportType' = 'Quarterly'), + 0::bigint, + 'When the committing form change archives a quarterly report, the corresponding form change in the pending revision on that project is deleted' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), + 0::bigint, + 'When the committing form change removes a project manager, the corresponding form change in the pending revision on that project is deleted' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'funding_parameter_EP'), + 0::bigint, + 'When the committing form change discards the emission intensity report, the corresponding form change in the pending revision on that project is deleted' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + 0::bigint, + 'When the committing form change is discarding a project attachment, the pending fc is deleted.' +); + +-- Commit the pending ammednment + +select lives_ok ( + $$ + select cif.commit_project_revision(2) + $$, + 'Committing the pending project_revision does not throw an error' +); + +select is ( + (select count(*) from cif.form_change where form_data_record_id is null), + 0::bigint, + 'All of the committed form_change records have a form_data_record_id assigned after pending is committed.' +); + + +select finish(); + +rollback;