From 52baf1221d6aff32cabe3656d6ecd0398e5a89db Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 30 Oct 2023 14:32:41 -0700 Subject: [PATCH 01/51] chore: add jsonb minus --- schema/deploy/functions/jsonb_minus.sql | 31 +++++++++++++++++++ schema/revert/functions/jsonb_minus.sql | 7 +++++ schema/sqitch.plan | 1 + .../test/unit/functions/jsonb_minus_test.sql | 27 ++++++++++++++++ schema/verify/functions/jsonb_minus.sql | 7 +++++ 5 files changed, 73 insertions(+) create mode 100644 schema/deploy/functions/jsonb_minus.sql create mode 100644 schema/revert/functions/jsonb_minus.sql create mode 100644 schema/test/unit/functions/jsonb_minus_test.sql create mode 100644 schema/verify/functions/jsonb_minus.sql diff --git a/schema/deploy/functions/jsonb_minus.sql b/schema/deploy/functions/jsonb_minus.sql new file mode 100644 index 0000000000..c3e2731118 --- /dev/null +++ b/schema/deploy/functions/jsonb_minus.sql @@ -0,0 +1,31 @@ +-- Deploy cif:functions/jsonb_minus to pg + +begin; +-- A note on the functionality: +-- If a key is present in the subtrahend but not the minuend, it will not appear in the result set. +-- {"a": 1, "b": 3} - {"a": 1, "b": 2, "c": 3} = {"b": 3} +-- If however a key is present in the minuend but not the subtrahend, it will appear in the result set with its value. +-- {"a": 1, "b": 3, "c": 3} - {"a": 1, "b": 2} = {"b": 3, "c": 3} + +-- This behaviour fits our needs at the time of writing this, so the additional complexity of handling the other cases is not needed. + + +create or replace function cif.jsonb_minus(minuend jsonb, subtrahend jsonb) + returns jsonb as +$$ +declare + difference jsonb; +begin + select jsonb_object_agg(key, value) into strict difference + from ( + select * from jsonb_each($1) + except select * from jsonb_each($2) + ) as temp; + + return difference; +end +$$ language plpgsql volatile; + +comment on function cif.pending_new_form_change_for_table(text) is + 'returns list of key-value pairs present in the first argument but not the second argument'; +commit; diff --git a/schema/revert/functions/jsonb_minus.sql b/schema/revert/functions/jsonb_minus.sql new file mode 100644 index 0000000000..4057447876 --- /dev/null +++ b/schema/revert/functions/jsonb_minus.sql @@ -0,0 +1,7 @@ +-- Revert cif:functions/jsonb_minus from pg + +begin; + +drop function if exists cif.jsonb_minus(jsonb, jsonb); + +commit; diff --git a/schema/sqitch.plan b/schema/sqitch.plan index e457b1c6e7..35839f6e16 100644 --- a/schema/sqitch.plan +++ b/schema/sqitch.plan @@ -366,3 +366,4 @@ functions/handle_milestone_form_change_commit [functions/handle_milestone_form_c @1.16.0 2023-10-24T17:15:40Z Dylan Leard # release v1.16.0 @1.16.1 2023-11-22T17:01:35Z Dylan Leard # release v1.16.1 @1.16.2 2024-02-23T22:00:38Z Pierre Bastianelli # release v1.16.2 +functions/jsonb_minus 2023-10-30T19:55:55Z Mike Vesprini # Function to provide the object difference between two jsonb objects diff --git a/schema/test/unit/functions/jsonb_minus_test.sql b/schema/test/unit/functions/jsonb_minus_test.sql new file mode 100644 index 0000000000..9abb42787d --- /dev/null +++ b/schema/test/unit/functions/jsonb_minus_test.sql @@ -0,0 +1,27 @@ +begin; + +select plan(4); + +select has_function('cif', 'jsonb_minus', 'function cif.jsonb_minus exists'); + +select is( + (select * from cif.jsonb_minus('{"a": 0, "b": 0, "c": 0}'::jsonb, '{"a": 0, "b": 0, "c": 0}'::jsonb)), + NULL, + 'The difference of two identical objects is NULL' +); + +select is( + (select * from cif.jsonb_minus('{"a": 1, "b": 0, "c": null}'::jsonb, '{"a": 0, "b": 0, "c": 0}'::jsonb)), + '{"a": 1, "c": null}'::jsonb, + 'All fields that are changed are returned' +); + +select is( + (select * from cif.jsonb_minus('{"a": 1, "b": 0, "c": 1, "d": null}'::jsonb, '{"a": 0, "b": 0}'::jsonb)), + '{"a": 1, "c": 1, "d": null}'::jsonb, + 'Added fields are included in the retuen value, including when their value is null' +); + +select finish(); + +rollback; diff --git a/schema/verify/functions/jsonb_minus.sql b/schema/verify/functions/jsonb_minus.sql new file mode 100644 index 0000000000..9f07ff4488 --- /dev/null +++ b/schema/verify/functions/jsonb_minus.sql @@ -0,0 +1,7 @@ +-- Verify cif:functions/jsonb_minus on pg + +begin; + +select pg_get_functiondef('cif.jsonb_minus(jsonb, jsonb)'::regprocedure); + +rollback; From 39b47cd2f771d233542fcc588ca48d62fdf90d64 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 30 Oct 2023 15:06:53 -0700 Subject: [PATCH 02/51] chore: rework setup for commit_form_change_internal --- .../commit_form_change_internal@1.15.0.sql | 37 +++++++++++++++++++ .../commit_form_change_internal@1.15.0.sql | 32 ++++++++++++++++ schema/sqitch.plan | 1 + .../commit_form_change_internal@1.15.0.sql | 7 ++++ 4 files changed, 77 insertions(+) create mode 100644 schema/deploy/mutations/commit_form_change_internal@1.15.0.sql create mode 100644 schema/revert/mutations/commit_form_change_internal@1.15.0.sql create mode 100644 schema/verify/mutations/commit_form_change_internal@1.15.0.sql diff --git a/schema/deploy/mutations/commit_form_change_internal@1.15.0.sql b/schema/deploy/mutations/commit_form_change_internal@1.15.0.sql new file mode 100644 index 0000000000..bee73b915f --- /dev/null +++ b/schema/deploy/mutations/commit_form_change_internal@1.15.0.sql @@ -0,0 +1,37 @@ +-- Deploy cif:mutations/commit_form_change to pg +begin; + +create or replace function cif_private.commit_form_change_internal(fc cif.form_change) + returns cif.form_change as $$ +declare + recordId int; +begin + + if fc.validation_errors != '[]' then + raise exception 'Cannot commit change with validation errors: %', fc.validation_errors; + end if; + + if fc.change_status = 'committed' then + raise exception 'Cannot commit form_change. It has already been committed.'; + end if; + + -- TODO : add a conditional behaviour based on fc.form_id + execute format( + 'select "cif_private".%I($1)', + (select form_change_commit_handler from cif.form where slug = fc.json_schema_name) + ) using fc into recordId; + + update cif.form_change set + form_data_record_id = recordId, + change_status = 'committed' + where id = fc.id; + + return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); +end; + $$ language plpgsql volatile; + +grant execute on function cif_private.commit_form_change_internal to cif_internal, cif_external, cif_admin; + +comment on function cif_private.commit_form_change_internal(cif.form_change) is 'Commits the form change and calls the corresponding commit handler.'; + +commit; diff --git a/schema/revert/mutations/commit_form_change_internal@1.15.0.sql b/schema/revert/mutations/commit_form_change_internal@1.15.0.sql new file mode 100644 index 0000000000..ae1a6bc0eb --- /dev/null +++ b/schema/revert/mutations/commit_form_change_internal@1.15.0.sql @@ -0,0 +1,32 @@ +-- Deploy cif:mutations/commit_form_change to pg +begin; + +create or replace function cif_private.commit_form_change_internal(fc cif.form_change) + returns cif.form_change as $$ +begin + + if fc.validation_errors != '[]' then + raise exception 'Cannot commit change with validation errors: %', fc.validation_errors; + end if; + + if fc.change_status = 'committed' then + raise exception 'Cannot commit form_change. It has already been committed.'; + end if; + + -- TODO : add a conditional behaviour based on fc.form_id + update cif.form_change set + form_data_record_id = ( + select cif_private.handle_default_form_change_commit(fc) + ), + change_status = 'committed' + where id = fc.id; + + return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); +end; + $$ language plpgsql volatile; + +grant execute on function cif_private.commit_form_change_internal to cif_internal, cif_external, cif_admin; + +comment on function cif_private.commit_form_change_internal(cif.form_change) is 'Commits the form change and calls the corresponding commit handler.'; + +commit; diff --git a/schema/sqitch.plan b/schema/sqitch.plan index 35839f6e16..ae285e5a40 100644 --- a/schema/sqitch.plan +++ b/schema/sqitch.plan @@ -367,3 +367,4 @@ functions/handle_milestone_form_change_commit [functions/handle_milestone_form_c @1.16.1 2023-11-22T17:01:35Z Dylan Leard # release v1.16.1 @1.16.2 2024-02-23T22:00:38Z Pierre Bastianelli # release v1.16.2 functions/jsonb_minus 2023-10-30T19:55:55Z Mike Vesprini # Function to provide the object difference between two jsonb objects +mutations/commit_form_change_internal [mutations/commit_form_change_internal@1.15.0] 2023-10-30T21:34:15Z Mike Vesprini # Handle rebasing when committing with another pending revision on the same project" diff --git a/schema/verify/mutations/commit_form_change_internal@1.15.0.sql b/schema/verify/mutations/commit_form_change_internal@1.15.0.sql new file mode 100644 index 0000000000..e45d9fa700 --- /dev/null +++ b/schema/verify/mutations/commit_form_change_internal@1.15.0.sql @@ -0,0 +1,7 @@ +-- Verify cif:mutations/commit_form_change on pg + +begin; + +select pg_get_functiondef('cif_private.commit_form_change_internal(cif.form_change)'::regprocedure); + +rollback; From 921f579e35c1f82cd5bd3b50370b78539872008c Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 31 Oct 2023 16:39:41 -0700 Subject: [PATCH 03/51] chore: pass correct arguments to commit form change internal --- .../mutations/commit_project_revision.sql | 16 +++++-- .../commit_project_revision@1.15.0.sql | 42 +++++++++++++++++ .../mutations/commit_project_revision.sql | 5 -- .../commit_project_revision@1.15.0.sql | 47 +++++++++++++++++++ .../commit_project_revision@1.15.0.sql | 7 +++ 5 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 schema/deploy/mutations/commit_project_revision@1.15.0.sql create mode 100644 schema/revert/mutations/commit_project_revision@1.15.0.sql create mode 100644 schema/verify/mutations/commit_project_revision@1.15.0.sql diff --git a/schema/deploy/mutations/commit_project_revision.sql b/schema/deploy/mutations/commit_project_revision.sql index bafe70a0cc..476ef710dc 100644 --- a/schema/deploy/mutations/commit_project_revision.sql +++ b/schema/deploy/mutations/commit_project_revision.sql @@ -4,23 +4,33 @@ begin; create or replace function cif.commit_project_revision(revision_to_commit_id int) returns cif.project_revision as $$ +declare + proj_id int; + pending_project_revision_id int; begin -- defer FK constraints check to the end of the transaction set constraints all deferred; + select form_data_record_id into proj_id from cif.form_change where form_data_table_name='project' and project_revision_id=$1; + select id into pending_project_revision_id from cif.project_revision + where project_id = proj_id + and change_status = 'pending' + and id != $1 + limit 1; + -- Propagate the change_status to all related form_change records -- Save the project table first to avoid foreign key violations from other potential tables. - perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change, pending_project_revision_id) from cif.form_change where project_revision_id=$1 and form_data_table_name='project'; - perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change, pending_project_revision_id) from cif.form_change where project_revision_id=$1 and form_data_table_name='reporting_requirement'; - perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change, pending_project_revision_id) from cif.form_change where project_revision_id=$1 and form_data_table_name not in ('project', 'reporting_requirement'); diff --git a/schema/deploy/mutations/commit_project_revision@1.15.0.sql b/schema/deploy/mutations/commit_project_revision@1.15.0.sql new file mode 100644 index 0000000000..bafe70a0cc --- /dev/null +++ b/schema/deploy/mutations/commit_project_revision@1.15.0.sql @@ -0,0 +1,42 @@ +-- Deploy cif:mutations/commit_project_revision to pg + +begin; + +create or replace function cif.commit_project_revision(revision_to_commit_id int) +returns cif.project_revision as $$ +begin + -- defer FK constraints check to the end of the transaction + set constraints all deferred; + + -- Propagate the change_status to all related form_change records + -- Save the project table first to avoid foreign key violations from other potential tables. + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name='project'; + + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name='reporting_requirement'; + + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name not in ('project', 'reporting_requirement'); + + update cif.project_revision set + project_id=(select form_data_record_id from cif.form_change where form_data_table_name='project' and project_revision_id=$1), + change_status='committed', + revision_status = 'Applied' + where id=$1; + + return (select row(project_revision.*)::cif.project_revision from cif.project_revision where id = $1); +end; +$$ language plpgsql; + +grant execute on function cif.commit_project_revision to cif_internal, cif_external, cif_admin; + +comment on function cif.commit_project_revision(int) is 'Commits a project_revision and all of its form changes'; + +commit; diff --git a/schema/revert/mutations/commit_project_revision.sql b/schema/revert/mutations/commit_project_revision.sql index e9549cacd9..bafe70a0cc 100644 --- a/schema/revert/mutations/commit_project_revision.sql +++ b/schema/revert/mutations/commit_project_revision.sql @@ -8,11 +8,6 @@ begin -- defer FK constraints check to the end of the transaction set constraints all deferred; - if ((select project_id from cif.project_revision where id = $1) is not null) - and ((select change_reason from cif.project_revision where id = $1) is null) then - raise exception 'Cannot commit revision if change_reason is null.'; - end if; - -- Propagate the change_status to all related form_change records -- Save the project table first to avoid foreign key violations from other potential tables. perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) diff --git a/schema/revert/mutations/commit_project_revision@1.15.0.sql b/schema/revert/mutations/commit_project_revision@1.15.0.sql new file mode 100644 index 0000000000..e9549cacd9 --- /dev/null +++ b/schema/revert/mutations/commit_project_revision@1.15.0.sql @@ -0,0 +1,47 @@ +-- Deploy cif:mutations/commit_project_revision to pg + +begin; + +create or replace function cif.commit_project_revision(revision_to_commit_id int) +returns cif.project_revision as $$ +begin + -- defer FK constraints check to the end of the transaction + set constraints all deferred; + + if ((select project_id from cif.project_revision where id = $1) is not null) + and ((select change_reason from cif.project_revision where id = $1) is null) then + raise exception 'Cannot commit revision if change_reason is null.'; + end if; + + -- Propagate the change_status to all related form_change records + -- Save the project table first to avoid foreign key violations from other potential tables. + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name='project'; + + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name='reporting_requirement'; + + perform cif_private.commit_form_change_internal(row(form_change.*)::cif.form_change) + from cif.form_change + where project_revision_id=$1 + and form_data_table_name not in ('project', 'reporting_requirement'); + + update cif.project_revision set + project_id=(select form_data_record_id from cif.form_change where form_data_table_name='project' and project_revision_id=$1), + change_status='committed', + revision_status = 'Applied' + where id=$1; + + return (select row(project_revision.*)::cif.project_revision from cif.project_revision where id = $1); +end; +$$ language plpgsql; + +grant execute on function cif.commit_project_revision to cif_internal, cif_external, cif_admin; + +comment on function cif.commit_project_revision(int) is 'Commits a project_revision and all of its form changes'; + +commit; diff --git a/schema/verify/mutations/commit_project_revision@1.15.0.sql b/schema/verify/mutations/commit_project_revision@1.15.0.sql new file mode 100644 index 0000000000..efa347aea8 --- /dev/null +++ b/schema/verify/mutations/commit_project_revision@1.15.0.sql @@ -0,0 +1,7 @@ +-- Verify cif:mutations/commit_project_revision on pg + +begin; + +select pg_get_functiondef('cif.commit_project_revision(int)'::regprocedure); + +rollback; From 983ad506cf45862a717425152ca8ca8a054fc9bb Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 31 Oct 2023 16:41:38 -0700 Subject: [PATCH 04/51] chore: rebase changes made in pending form change on top of the committing form change --- .../mutations/commit_form_change_internal.sql | 42 +++++++++++++++++-- .../mutations/commit_form_change_internal.sql | 11 +++-- schema/sqitch.plan | 1 + 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index bee73b915f..2eb823e4d5 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -1,10 +1,12 @@ -- Deploy cif:mutations/commit_form_change to pg begin; -create or replace function cif_private.commit_form_change_internal(fc cif.form_change) +create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int) returns cif.form_change as $$ declare recordId int; + pending_form_change cif.form_change; + parent_of_pending_form_change cif.form_change; begin if fc.validation_errors != '[]' then @@ -26,12 +28,46 @@ begin change_status = 'committed' where id = fc.id; + if pending_project_revision_id is not null then + -- If the committing form change is a create, then it needs to be created in the pending revision with an update operation. + if fc.operation = 'create' then + perform cif.create_form_change( + operation => 'update'::cif.form_change_operation, + form_data_schema_name => 'cif', + form_data_table_name => fc.form_data_table_name, + form_data_record_id => fc.form_data_record_id, + project_revision_id => pending_project_revision_id, + json_schema_name => fc.json_schema_name, + new_form_data => fc.new_form_data + ); + elsif fc.operation = 'update' then + -- store the other pending project revisions corresponding form_change, and its parent + select * into pending_form_change from cif.form_change + where project_revision_id = pending_project_revision_id + and form_data_table_name = fc.form_data_table_name + and form_data_record_id = fc.form_data_record_id limit 1; + select * into parent_of_pending_form_change from cif.form_change + where id = pending_form_change.previous_form_change_id limit 1; + -- set the pending form change data to be the committing form change data, plus the changes made in the + -- pending revision + update cif.form_change + set new_form_data = + (fc.new_form_data || cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) + where id = pending_form_change.id; + + elsif fc.operation = 'archive' then + delete from cif.form_change + where project_revision_id = pending_project_revision_id + and form_data_table_name = fc.form_data_table_name + and form_data_record_id = fc.form_data_record_id; + end if; + end if; return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); end; $$ language plpgsql volatile; -grant execute on function cif_private.commit_form_change_internal to cif_internal, cif_external, cif_admin; +grant execute on function cif_private.commit_form_change_internal(cif.form_change, int) to cif_internal, cif_external, cif_admin; -comment on function cif_private.commit_form_change_internal(cif.form_change) is 'Commits the form change and calls the corresponding commit handler.'; +comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler.'; commit; diff --git a/schema/revert/mutations/commit_form_change_internal.sql b/schema/revert/mutations/commit_form_change_internal.sql index ae1a6bc0eb..bee73b915f 100644 --- a/schema/revert/mutations/commit_form_change_internal.sql +++ b/schema/revert/mutations/commit_form_change_internal.sql @@ -3,6 +3,8 @@ begin; create or replace function cif_private.commit_form_change_internal(fc cif.form_change) returns cif.form_change as $$ +declare + recordId int; begin if fc.validation_errors != '[]' then @@ -14,10 +16,13 @@ begin end if; -- TODO : add a conditional behaviour based on fc.form_id + execute format( + 'select "cif_private".%I($1)', + (select form_change_commit_handler from cif.form where slug = fc.json_schema_name) + ) using fc into recordId; + update cif.form_change set - form_data_record_id = ( - select cif_private.handle_default_form_change_commit(fc) - ), + form_data_record_id = recordId, change_status = 'committed' where id = fc.id; diff --git a/schema/sqitch.plan b/schema/sqitch.plan index ae285e5a40..25785ecc39 100644 --- a/schema/sqitch.plan +++ b/schema/sqitch.plan @@ -368,3 +368,4 @@ functions/handle_milestone_form_change_commit [functions/handle_milestone_form_c @1.16.2 2024-02-23T22:00:38Z Pierre Bastianelli # release v1.16.2 functions/jsonb_minus 2023-10-30T19:55:55Z Mike Vesprini # Function to provide the object difference between two jsonb objects mutations/commit_form_change_internal [mutations/commit_form_change_internal@1.15.0] 2023-10-30T21:34:15Z Mike Vesprini # Handle rebasing when committing with another pending revision on the same project" +mutations/commit_project_revision [mutations/commit_project_revision@1.15.0] 2023-10-31T00:07:01Z Mike Vesprini # Update function to pass new parameters to commit_form_change_internal From 34c77b914a2a17b9caf6c669aa1dfe7a6e134ed1 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 9 Nov 2023 14:20:07 -0800 Subject: [PATCH 05/51] chore: handle the merging of form changes when another project revision is pending on the project --- app/schema/schema.graphql | 34 ++++- app/schema/schema.json | 125 +++++++++++++++++- .../mutations/commit_form_change_internal.sql | 31 ++++- 3 files changed, 182 insertions(+), 8 deletions(-) diff --git a/app/schema/schema.graphql b/app/schema/schema.graphql index 4c54e30c9e..9ad7819c92 100644 --- a/app/schema/schema.graphql +++ b/app/schema/schema.graphql @@ -50871,6 +50871,32 @@ input JSONFilter { notIn: [JSON!] } +"""All input for the `jsonbMinus` mutation.""" +input JsonbMinusInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + minuend: JSON! + subtrahend: JSON! +} + +"""The output of our `jsonbMinus` mutation.""" +type JsonbMinusPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + json: JSON + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + type KeycloakJwt { acr: String aud: String @@ -52297,6 +52323,12 @@ type Mutation { """ input: GenerateQuarterlyReportsInput! ): GenerateQuarterlyReportsPayload + jsonbMinus( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: JsonbMinusInput! + ): JsonbMinusPayload stageDirtyFormChanges( """ The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. @@ -66928,7 +66960,7 @@ type Query implements Node { paymentByRowId(rowId: Int!): Payment """ - returns a form_change for a table in the pending state for the current user, i.e. allows to resume the creation of any table row + returns list of key-value pairs present in the first argument but not the second argument """ pendingNewFormChangeForTable(tableName: String!): FormChange diff --git a/app/schema/schema.json b/app/schema/schema.json index caf884b9ae..0a319f9f41 100644 --- a/app/schema/schema.json +++ b/app/schema/schema.json @@ -171233,6 +171233,102 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "JsonbMinusInput", + "description": "All input for the `jsonbMinus` mutation.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An arbitrary string value with no semantic meaning. Will be included in the\npayload verbatim. May be used to track mutations by the client.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "minuend", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "subtrahend", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "JsonbMinusPayload", + "description": "The output of our `jsonbMinus` mutation.", + "fields": [ + { + "name": "clientMutationId", + "description": "The exact same `clientMutationId` that was provided in the mutation input,\nunchanged and unused. May be used by a client to track mutations.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "json", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "Our root query field type. Allows us to run any query from our mutation payload.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Query", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "KeycloakJwt", @@ -176610,6 +176706,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "jsonbMinus", + "description": null, + "args": [ + { + "name": "input", + "description": "The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "JsonbMinusInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "JsonbMinusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "stageDirtyFormChanges", "description": null, @@ -229591,7 +229714,7 @@ }, { "name": "pendingNewFormChangeForTable", - "description": "returns a form_change for a table in the pending state for the current user, i.e. allows to resume the creation of any table row", + "description": "returns list of key-value pairs present in the first argument but not the second argument", "args": [ { "name": "tableName", diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 2eb823e4d5..eaabad0d50 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -7,6 +7,8 @@ declare recordId int; pending_form_change cif.form_change; parent_of_pending_form_change cif.form_change; + pending_minus_pendings_parent jsonb; + committing_minus_pendings_parent jsonb; begin if fc.validation_errors != '[]' then @@ -48,12 +50,29 @@ begin and form_data_record_id = fc.form_data_record_id limit 1; select * into parent_of_pending_form_change from cif.form_change where id = pending_form_change.previous_form_change_id limit 1; - -- set the pending form change data to be the committing form change data, plus the changes made in the - -- pending revision - update cif.form_change - set new_form_data = - (fc.new_form_data || cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) - where id = pending_form_change.id; + + select (cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) + into pending_minus_pendings_parent; + select (cif.jsonb_minus(fc.new_form_data, parent_of_pending_form_change.new_form_data)) + into committing_minus_pendings_parent; + + if committing_minus_pendings_parent is not null then + if pending_minus_pendings_parent is not null then + -- if the committing and pending form changes both have changes from the pending form change's parent, + -- then set the pending form change to be the committing form change, plus the changes made in the penging form change. + update cif.form_change + set new_form_data = + (fc.new_form_data || pending_minus_pendings_parent) + where id = pending_form_change.id; + else + -- The pending form change hasn't made any changes since its creation, but the committing form change has. + -- Set the pending form change ot be the committing form change as it is the latest information + update cif.form_change + set new_form_data = + (fc.new_form_data) + where id = pending_form_change.id; + end if; + end if; elsif fc.operation = 'archive' then delete from cif.form_change From 9e5ee2f60aeab26a326109384e47bb3eb8cc5cf4 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 16 Nov 2023 16:00:49 -0800 Subject: [PATCH 06/51] test: initial tests for concurrent revisions merging --- .../mutations/commit_form_change_internal.sql | 2 +- .../commit_project_revision_test.sql | 99 ++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index eaabad0d50..1c0d40ca91 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -43,7 +43,7 @@ begin new_form_data => fc.new_form_data ); elsif fc.operation = 'update' then - -- store the other pending project revisions corresponding form_change, and its parent + -- store the pending project revisions corresponding form_change, and its parent select * into pending_form_change from cif.form_change where project_revision_id = pending_project_revision_id and form_data_table_name = fc.form_data_table_name diff --git a/schema/test/unit/mutations/commit_project_revision_test.sql b/schema/test/unit/mutations/commit_project_revision_test.sql index 6d4293f7d3..4fe817e1e7 100644 --- a/schema/test/unit/mutations/commit_project_revision_test.sql +++ b/schema/test/unit/mutations/commit_project_revision_test.sql @@ -1,6 +1,6 @@ begin; -select plan(9); +select plan(13); /** BEGIN SETUP **/ truncate table @@ -24,6 +24,7 @@ restart identity; 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.contact(given_name, family_name, email) values ('Mira', 'Test', 'bar@abc.com'); select cif.create_project(1); @@ -210,6 +211,102 @@ select results_eq( 'commit_project_revision sets revision_status to Applied when revision_type is General Revision' ); +-- Test the concurrent revision functinality + +truncate cif.project restart identity cascade; + +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 +update cif.form_change set new_form_data='{ + "projectName": "Correct", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=2 + and form_data_table_name='project'; + +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +update cif.form_change set new_form_data='{ + "projectName": "Incorrect", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=3 + and form_data_table_name='project'; + +insert into cif.form_change( + new_form_data, + operation, + form_data_schema_name, + form_data_table_name, + json_schema_name, + project_revision_id +) + values +( + json_build_object( + 'projectId', 1, + 'contactId', 1, + 'contactIndex', 1 + ), + 'create', 'cif', 'project_contact', 'project_contact', 3 +); + +select lives_ok ( + $$ + select cif.commit_project_revision(3) + $$, + 'The General Revision successfully commits while there is a pending Amendment on the project' +); + +select lives_ok ( + $$ + select cif.commit_project_revision(2) + $$, + 'The Amendment successfully commits after a General Revision being committed while the Amendment was pending' +); + +select is ( + (select new_form_data from cif.form_change where + project_revision_id=2 + and form_data_table_name='project'), + '{ + "projectName": "Correct", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb, + 'The project form_change has the correct data after the Amendment is committed' +); + +select isnt_empty ( + $$ + select id from cif.project_contact where project_id=1; + $$, + 'The project_contact added in the General Revision was succesfully added after the Amendment was committed' +); + select finish(); rollback; From 52e23016e52c30dcd71f6853aa8f31a419fbe8c9 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Fri, 17 Nov 2023 17:44:46 -0800 Subject: [PATCH 07/51] test: test the updates of the simple forms in commit_form_change_internal --- .../commit_form_change_internal_test.sql | 81 ++++++++++++++++++- .../commit_project_revision_test.sql | 1 - 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 3fa3a42583..aaad949fc2 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(6); +select plan(10); /** SETUP **/ truncate cif.form_change restart identity; @@ -77,7 +77,84 @@ select is( 'The form_change status should be committed' ); --- Calls the proper function set in the form table + +-- Test the concurrent revision functinality + +truncate table cif.project, cif.operator 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'); + +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 +update cif.form_change set new_form_data='{ + "projectName": "Correct", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=2 + and form_data_table_name='project'; + +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +update cif.form_change set new_form_data='{ + "projectName": "Incorrect", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=3 + and form_data_table_name='project'; + +select cif.commit_project_revision(3); + +select is ( + (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' +); + +select is ( + (select project_name from cif.project where id = 1), + 'Incorrect', + 'The project receives the value from the committing form change' +); + +select is ( + (select new_form_data->>'summary' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' +); + +-- Commit the ammednment +select cif.commit_project_revision(2); + +select results_eq ( + $$ + (select project_name, summary, funding_stream_rfp_id, project_status_id, proposal_reference, operator_id from cif.project where id = 1) + $$, + $$ + values('Correct'::varchar, 'Correct'::varchar, 1::int, 1::int, '1235'::varchar, 1::int) + $$, + 'After committing the pending form change, the project table has all of the correct values' +); + select finish(); diff --git a/schema/test/unit/mutations/commit_project_revision_test.sql b/schema/test/unit/mutations/commit_project_revision_test.sql index 4fe817e1e7..3021fd3939 100644 --- a/schema/test/unit/mutations/commit_project_revision_test.sql +++ b/schema/test/unit/mutations/commit_project_revision_test.sql @@ -24,7 +24,6 @@ restart identity; 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.contact(given_name, family_name, email) values ('Mira', 'Test', 'bar@abc.com'); select cif.create_project(1); From 6a1bcad257fd3848acf82b9fa5436842a946dc01 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 20 Nov 2023 15:11:23 -0800 Subject: [PATCH 08/51] chore: fix revert sqitch function --- schema/revert/mutations/commit_form_change_internal.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/revert/mutations/commit_form_change_internal.sql b/schema/revert/mutations/commit_form_change_internal.sql index bee73b915f..3d40a9c194 100644 --- a/schema/revert/mutations/commit_form_change_internal.sql +++ b/schema/revert/mutations/commit_form_change_internal.sql @@ -30,7 +30,7 @@ begin end; $$ language plpgsql volatile; -grant execute on function cif_private.commit_form_change_internal to cif_internal, cif_external, cif_admin; +grant execute on function cif_private.commit_form_change_internal(fc cif.form_change) to cif_internal, cif_external, cif_admin; comment on function cif_private.commit_form_change_internal(cif.form_change) is 'Commits the form change and calls the corresponding commit handler.'; From 371807f8222f870fe6cbe7334b06a05eb0d502c6 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 20 Nov 2023 16:03:52 -0800 Subject: [PATCH 09/51] chore: fix signature in rever --- schema/revert/mutations/commit_form_change_internal.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/schema/revert/mutations/commit_form_change_internal.sql b/schema/revert/mutations/commit_form_change_internal.sql index 3d40a9c194..1e1e8c5f9a 100644 --- a/schema/revert/mutations/commit_form_change_internal.sql +++ b/schema/revert/mutations/commit_form_change_internal.sql @@ -1,6 +1,7 @@ -- Deploy cif:mutations/commit_form_change to pg begin; +-- drop function if exists cif_private.commit_form_change_internal(cif.form_change, int); create or replace function cif_private.commit_form_change_internal(fc cif.form_change) returns cif.form_change as $$ declare @@ -30,7 +31,7 @@ begin end; $$ language plpgsql volatile; -grant execute on function cif_private.commit_form_change_internal(fc cif.form_change) to cif_internal, cif_external, cif_admin; +grant execute on function cif_private.commit_form_change_internal(cif.form_change) to cif_internal, cif_external, cif_admin; comment on function cif_private.commit_form_change_internal(cif.form_change) is 'Commits the form change and calls the corresponding commit handler.'; From ac8c74de77fcff53ff7807b452586bc255becded Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 20 Nov 2023 17:21:11 -0800 Subject: [PATCH 10/51] chore: explicitly drop the old function signature --- schema/deploy/mutations/commit_form_change_internal.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 1c0d40ca91..f017474906 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -1,6 +1,8 @@ -- Deploy cif:mutations/commit_form_change to pg begin; +-- We need to explicitly drop the old function here since we're changing the signature. +drop function cif_private.commit_form_change_internal(cif.form_change); create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int) returns cif.form_change as $$ declare From e83dfee9c78a82fca25a7c3899c6110b9618229a Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 20 Nov 2023 17:22:36 -0800 Subject: [PATCH 11/51] test: add create and mutual update tests --- .../mutations/commit_form_change_internal.sql | 1 - .../mutations/commit_form_change_internal_test.sql | 13 +++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/schema/revert/mutations/commit_form_change_internal.sql b/schema/revert/mutations/commit_form_change_internal.sql index 1e1e8c5f9a..801d43f9d5 100644 --- a/schema/revert/mutations/commit_form_change_internal.sql +++ b/schema/revert/mutations/commit_form_change_internal.sql @@ -1,7 +1,6 @@ -- Deploy cif:mutations/commit_form_change to pg begin; --- drop function if exists cif_private.commit_form_change_internal(cif.form_change, int); create or replace function cif_private.commit_form_change_internal(fc cif.form_change) returns cif.form_change as $$ declare diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index aaad949fc2..fb29d9d902 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(10); +select plan(11); /** SETUP **/ truncate cif.form_change restart identity; @@ -80,7 +80,7 @@ select is( -- Test the concurrent revision functinality -truncate table cif.project, cif.operator restart identity cascade; +truncate table cif.project, cif.operator, cif.contact 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'); @@ -122,8 +122,11 @@ update cif.form_change set new_form_data='{ where project_revision_id=3 and form_data_table_name='project'; +select cif.add_contact_to_revision(3, 1, 1); + select cif.commit_project_revision(3); +-- Both committing and pending project revisions have made changes to the project form. select is ( (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), 'Correct', @@ -142,6 +145,12 @@ select is ( 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' ); +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, + 'When the committing form change has an operation create, the resource also gets created in the pending revision' +); + -- Commit the ammednment select cif.commit_project_revision(2); From 29afc509ed7fa3b2de0864f35788432ff2135f42 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 21 Nov 2023 13:58:18 -0800 Subject: [PATCH 12/51] chore: explicitly drop other function signature in revert file --- schema/revert/mutations/commit_form_change_internal.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/schema/revert/mutations/commit_form_change_internal.sql b/schema/revert/mutations/commit_form_change_internal.sql index 801d43f9d5..459659eb76 100644 --- a/schema/revert/mutations/commit_form_change_internal.sql +++ b/schema/revert/mutations/commit_form_change_internal.sql @@ -1,6 +1,7 @@ -- Deploy cif:mutations/commit_form_change to pg begin; +drop function if exists cif_private.commit_form_change_internal(cif.form_change, int); create or replace function cif_private.commit_form_change_internal(fc cif.form_change) returns cif.form_change as $$ declare From d4d974ec557d35a340b56a6b774a9d44f83f9316 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 21 Nov 2023 15:07:34 -0800 Subject: [PATCH 13/51] chore: fix db deploy --- schema/deploy/mutations/commit_form_change_internal.sql | 4 ++-- schema/verify/mutations/commit_form_change_internal.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index f017474906..9847c11db1 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -2,8 +2,8 @@ begin; -- We need to explicitly drop the old function here since we're changing the signature. -drop function cif_private.commit_form_change_internal(cif.form_change); -create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int) +drop function if exists cif_private.commit_form_change_internal(fc cif.form_change); +create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int default null) returns cif.form_change as $$ declare recordId int; diff --git a/schema/verify/mutations/commit_form_change_internal.sql b/schema/verify/mutations/commit_form_change_internal.sql index e45d9fa700..ac5a71a4c5 100644 --- a/schema/verify/mutations/commit_form_change_internal.sql +++ b/schema/verify/mutations/commit_form_change_internal.sql @@ -2,6 +2,6 @@ begin; -select pg_get_functiondef('cif_private.commit_form_change_internal(cif.form_change)'::regprocedure); +select pg_get_functiondef('cif_private.commit_form_change_internal(cif.form_change, int)'::regprocedure); rollback; From d8f52b0038407c93b769f5ec197769af60ec64c9 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 21 Nov 2023 15:08:06 -0800 Subject: [PATCH 14/51] chore: switch test check from form change to project table, fc tested elsewhere --- .../commit_project_revision_test.sql | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/schema/test/unit/mutations/commit_project_revision_test.sql b/schema/test/unit/mutations/commit_project_revision_test.sql index 3021fd3939..237593d1a5 100644 --- a/schema/test/unit/mutations/commit_project_revision_test.sql +++ b/schema/test/unit/mutations/commit_project_revision_test.sql @@ -284,19 +284,14 @@ select lives_ok ( 'The Amendment successfully commits after a General Revision being committed while the Amendment was pending' ); -select is ( - (select new_form_data from cif.form_change where - project_revision_id=2 - and form_data_table_name='project'), - '{ - "projectName": "Correct", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb, - 'The project form_change has the correct data after the Amendment is committed' +select results_eq ( + $$ + (select project_name, summary from cif.project where id = 1 limit 1) + $$, + $$ + values("Correct", "Correct") + $$, + 'The project table has the correct data after the Amendment is committed' ); select isnt_empty ( From bbddafc166bbfe82fcf45c0b862070b0049cce3d Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 21 Nov 2023 15:31:26 -0800 Subject: [PATCH 15/51] test: explicit argument list in function should exist test --- schema/deploy/mutations/commit_form_change_internal.sql | 2 +- schema/test/unit/mutations/commit_form_change_internal_test.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 9847c11db1..6693329645 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -2,7 +2,7 @@ begin; -- We need to explicitly drop the old function here since we're changing the signature. -drop function if exists cif_private.commit_form_change_internal(fc cif.form_change); +drop function if exists cif_private.commit_form_change_internal(cif.form_change); create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int default null) returns cif.form_change as $$ declare diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index fb29d9d902..a31c6c39c4 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -35,7 +35,7 @@ values ( ; -- make sure the function exists -select has_function('cif_private', 'commit_form_change_internal', ARRAY['cif.form_change'], 'Function commit_form_change_internal should exist'); +select has_function('cif_private', 'commit_form_change_internal', ARRAY['cif.form_change', 'int'], 'Function commit_form_change_internal should exist'); select results_eq( $$ From e461f8075c9d3a0dd0b0df79edf636ca185d625d Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 22 Nov 2023 09:58:45 -0800 Subject: [PATCH 16/51] test: typecast values in test resturn --- schema/test/unit/mutations/commit_form_change_internal_test.sql | 2 ++ schema/test/unit/mutations/commit_project_revision_test.sql | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index a31c6c39c4..5e8f65bab0 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -164,6 +164,8 @@ select results_eq ( 'After committing the pending form change, the project table has all of the correct values' ); +-- Test when committing has made changes to the form but the pending has not + select finish(); diff --git a/schema/test/unit/mutations/commit_project_revision_test.sql b/schema/test/unit/mutations/commit_project_revision_test.sql index 237593d1a5..02e67d0fd2 100644 --- a/schema/test/unit/mutations/commit_project_revision_test.sql +++ b/schema/test/unit/mutations/commit_project_revision_test.sql @@ -289,7 +289,7 @@ select results_eq ( (select project_name, summary from cif.project where id = 1 limit 1) $$, $$ - values("Correct", "Correct") + values("Correct"::varchar, "Correct"::varchar) $$, 'The project table has the correct data after the Amendment is committed' ); From 1723f74aab810dfd69a16e4371bb84c4d7e682da Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 22 Nov 2023 10:29:52 -0800 Subject: [PATCH 17/51] chore: fix quotes in pg test --- schema/test/unit/mutations/commit_project_revision_test.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/test/unit/mutations/commit_project_revision_test.sql b/schema/test/unit/mutations/commit_project_revision_test.sql index 02e67d0fd2..44b16a5fcf 100644 --- a/schema/test/unit/mutations/commit_project_revision_test.sql +++ b/schema/test/unit/mutations/commit_project_revision_test.sql @@ -289,7 +289,7 @@ select results_eq ( (select project_name, summary from cif.project where id = 1 limit 1) $$, $$ - values("Correct"::varchar, "Correct"::varchar) + values('Correct'::varchar, 'Correct'::varchar) $$, 'The project table has the correct data after the Amendment is committed' ); From c51a4c3c351553e7410c8c4f8b06f3595d698313 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 22 Nov 2023 11:45:31 -0800 Subject: [PATCH 18/51] chore: handle the new commit_form_change_internal args in commit_form_change --- .../deploy/mutations/commit_form_change.sql | 7 +++++- .../mutations/commit_form_change@1.15.0.sql | 23 +++++++++++++++++++ .../revert/mutations/commit_form_change.sql | 20 ++++++++++++++-- .../mutations/commit_form_change@1.15.0.sql | 7 ++++++ schema/sqitch.plan | 1 + .../mutations/commit_form_change_test.sql | 6 ++--- .../mutations/commit_form_change@1.15.0.sql | 7 ++++++ 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 schema/deploy/mutations/commit_form_change@1.15.0.sql create mode 100644 schema/revert/mutations/commit_form_change@1.15.0.sql create mode 100644 schema/verify/mutations/commit_form_change@1.15.0.sql diff --git a/schema/deploy/mutations/commit_form_change.sql b/schema/deploy/mutations/commit_form_change.sql index 0ea3a5c4cd..1639040bea 100644 --- a/schema/deploy/mutations/commit_form_change.sql +++ b/schema/deploy/mutations/commit_form_change.sql @@ -12,7 +12,12 @@ begin validation_errors = coalesce(form_change_patch.validation_errors, validation_errors) where id=row_id; - return (select cif_private.commit_form_change_internal((select row(form_change.*)::cif.form_change from cif.form_change where id = row_id))); + return (select cif_private.commit_form_change_internal( + (select row(form_change.*)::cif.form_change from cif.form_change where id = row_id), + (select id from cif.project_revision + where project_id=(select project_id from cif.project_revision where id = form_change_patch.project_revision_id) + and change_status = 'pending' and id != form_change_patch.project_revision_id limit 1) + )); end; $$ language plpgsql volatile; diff --git a/schema/deploy/mutations/commit_form_change@1.15.0.sql b/schema/deploy/mutations/commit_form_change@1.15.0.sql new file mode 100644 index 0000000000..0ea3a5c4cd --- /dev/null +++ b/schema/deploy/mutations/commit_form_change@1.15.0.sql @@ -0,0 +1,23 @@ +-- Deploy cif:mutations/commit_form_change to pg +-- requires: tables/form_change + +begin; + +create or replace function cif.commit_form_change(row_id int, form_change_patch cif.form_change) + returns cif.form_change as $$ +begin + + update cif.form_change set + new_form_data = coalesce(form_change_patch.new_form_data, new_form_data), + validation_errors = coalesce(form_change_patch.validation_errors, validation_errors) + where id=row_id; + + return (select cif_private.commit_form_change_internal((select row(form_change.*)::cif.form_change from cif.form_change where id = row_id))); +end; + $$ language plpgsql volatile; + +grant execute on function cif.commit_form_change to cif_internal, cif_external, cif_admin; + +comment on function cif.commit_form_change is 'Custom mutation to commit a form_change record via the API. Only used for records that are independent of a project such as the lists of contacts and operators.'; + +commit; diff --git a/schema/revert/mutations/commit_form_change.sql b/schema/revert/mutations/commit_form_change.sql index a35011ad52..0ea3a5c4cd 100644 --- a/schema/revert/mutations/commit_form_change.sql +++ b/schema/revert/mutations/commit_form_change.sql @@ -1,7 +1,23 @@ --- Revert cif:mutations/commit_form_change from pg +-- Deploy cif:mutations/commit_form_change to pg +-- requires: tables/form_change begin; -drop function cif.commit_form_change; +create or replace function cif.commit_form_change(row_id int, form_change_patch cif.form_change) + returns cif.form_change as $$ +begin + + update cif.form_change set + new_form_data = coalesce(form_change_patch.new_form_data, new_form_data), + validation_errors = coalesce(form_change_patch.validation_errors, validation_errors) + where id=row_id; + + return (select cif_private.commit_form_change_internal((select row(form_change.*)::cif.form_change from cif.form_change where id = row_id))); +end; + $$ language plpgsql volatile; + +grant execute on function cif.commit_form_change to cif_internal, cif_external, cif_admin; + +comment on function cif.commit_form_change is 'Custom mutation to commit a form_change record via the API. Only used for records that are independent of a project such as the lists of contacts and operators.'; commit; diff --git a/schema/revert/mutations/commit_form_change@1.15.0.sql b/schema/revert/mutations/commit_form_change@1.15.0.sql new file mode 100644 index 0000000000..a35011ad52 --- /dev/null +++ b/schema/revert/mutations/commit_form_change@1.15.0.sql @@ -0,0 +1,7 @@ +-- Revert cif:mutations/commit_form_change from pg + +begin; + +drop function cif.commit_form_change; + +commit; diff --git a/schema/sqitch.plan b/schema/sqitch.plan index 25785ecc39..5f80208ae5 100644 --- a/schema/sqitch.plan +++ b/schema/sqitch.plan @@ -369,3 +369,4 @@ functions/handle_milestone_form_change_commit [functions/handle_milestone_form_c functions/jsonb_minus 2023-10-30T19:55:55Z Mike Vesprini # Function to provide the object difference between two jsonb objects mutations/commit_form_change_internal [mutations/commit_form_change_internal@1.15.0] 2023-10-30T21:34:15Z Mike Vesprini # Handle rebasing when committing with another pending revision on the same project" mutations/commit_project_revision [mutations/commit_project_revision@1.15.0] 2023-10-31T00:07:01Z Mike Vesprini # Update function to pass new parameters to commit_form_change_internal +mutations/commit_form_change [mutations/commit_form_change@1.15.0] 2023-11-22T19:00:00Z Mike Vesprini # Add pending project revision id to args of commit_form_change_internal call diff --git a/schema/test/unit/mutations/commit_form_change_test.sql b/schema/test/unit/mutations/commit_form_change_test.sql index b058250818..61765b0c2e 100644 --- a/schema/test/unit/mutations/commit_form_change_test.sql +++ b/schema/test/unit/mutations/commit_form_change_test.sql @@ -7,7 +7,7 @@ select plan(2); /** SETUP **/ truncate cif.form_change restart identity; -create or replace function cif_private.commit_form_change_internal(fc cif.form_change) +create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int default null) returns cif.form_change as $$ begin return fc; @@ -54,7 +54,7 @@ select results_eq( validation_errors from cif.commit_form_change( 12345, - (select row( null, '{"updated":true}', null, null, null, null, null, null, null, '["hazErrors"]', null, null, null, null, null)::cif.form_change) + (select row( null, '{"updated":true}', null, null, null, null, null, null, null, '[]', null, null, null, null, null)::cif.form_change) ); $$, $$ @@ -68,7 +68,7 @@ select results_eq( null::int, 'pending'::varchar, 'reporting_requirement'::varchar, - '["hazErrors"]'::jsonb + '[]'::jsonb ) $$, 'commit_form_change calls the private commit_form_change_internal() function' diff --git a/schema/verify/mutations/commit_form_change@1.15.0.sql b/schema/verify/mutations/commit_form_change@1.15.0.sql new file mode 100644 index 0000000000..beed54db86 --- /dev/null +++ b/schema/verify/mutations/commit_form_change@1.15.0.sql @@ -0,0 +1,7 @@ +-- Verify cif:mutations/commit_form_change on pg + +begin; + +select pg_get_functiondef('cif.commit_form_change(int, cif.form_change)'::regprocedure); + +rollback; From 1b0669b279fdcc36bfbf3b9e696e77622bdd4e81 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 22 Nov 2023 13:33:30 -0800 Subject: [PATCH 19/51] test: test the case when the commiting fc has made changes but the pending has not --- .../commit_form_change_internal_test.sql | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 5e8f65bab0..23ff6f24f7 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(11); +select plan(13); /** SETUP **/ truncate cif.form_change restart identity; @@ -165,7 +165,33 @@ select results_eq ( ); -- Test when committing has made changes to the form but the pending has not +select cif.create_project_revision(1, 'Amendment'); -- id = 4 +select cif.create_project_revision(1, 'General Revision'); -- id = 5 +update cif.form_change set new_form_data='{ + "projectName": "Correct only newer", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=5 + and form_data_table_name='project'; +select cif.commit_project_revision(5); + +select is ( + (select new_form_data->>'projectName' from cif.form_change where id = 6), + 'Correct only newer', + 'The pending form change should have the value from the committing form change' +); + +select cif.commit_project_revision(4); +select is ( + (select project_name from cif.project where id = 1), + 'Correct only newer', + 'The project table should have the updated proejct name, even after the pending amendment is committed' +); select finish(); From 8527c052144f866a6b7a941789834ae59c4bb63b Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 28 Nov 2023 11:35:48 -0800 Subject: [PATCH 20/51] test: test that attachments are handled correctly when concurrent revisions are committed --- .../commit_form_change_internal_test.sql | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 23ff6f24f7..ce82afce72 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(13); +select plan(15); /** SETUP **/ truncate cif.form_change restart identity; @@ -80,9 +80,11 @@ select is( -- Test the concurrent revision functinality -truncate table cif.project, cif.operator, cif.contact restart identity cascade; +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='{ @@ -123,6 +125,7 @@ update cif.form_change set new_form_data='{ and form_data_table_name='project'; select cif.add_contact_to_revision(3, 1, 1); +select cif.add_project_attachment_to_revision(3,1); select cif.commit_project_revision(3); @@ -148,7 +151,13 @@ select is ( select is ( (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, - 'When the committing form change has an operation create, the resource also gets created in the pending revision' + 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + 1::bigint, + 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' ); -- Commit the ammednment @@ -178,14 +187,22 @@ update cif.form_change set new_form_data='{ where project_revision_id=5 and form_data_table_name='project'; +select cif.discard_project_attachment_form_change((select id from cif.form_change where project_revision_id = 5 and form_data_table_name = 'project_attachment')); + select cif.commit_project_revision(5); select is ( - (select new_form_data->>'projectName' from cif.form_change where id = 6), + (select new_form_data->>'projectName' from cif.form_change where id = 8), 'Correct only newer', 'The pending form change should have the value from the committing form change' ); +select is ( + (select count(*) from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project_attachment'), + 0::bigint, + 'When the committing form change is discarding a project attachment, the pending fc is deleted.' +); + select cif.commit_project_revision(4); select is ( (select project_name from cif.project where id = 1), From c23fe072059a4e4b831eaf98efc5ddcbd94f9e33 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 30 Nov 2023 15:13:14 -0800 Subject: [PATCH 21/51] chore: set the previous_form_change_id on pending form changes --- schema/deploy/mutations/commit_form_change_internal.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 6693329645..fa5ed6649f 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -11,6 +11,7 @@ declare parent_of_pending_form_change cif.form_change; pending_minus_pendings_parent jsonb; committing_minus_pendings_parent jsonb; + new_fc_in_pending_id int; begin if fc.validation_errors != '[]' then @@ -35,7 +36,7 @@ begin if pending_project_revision_id is not null then -- If the committing form change is a create, then it needs to be created in the pending revision with an update operation. if fc.operation = 'create' then - perform cif.create_form_change( + select id into new_fc_in_pending_id from cif.create_form_change( operation => 'update'::cif.form_change_operation, form_data_schema_name => 'cif', form_data_table_name => fc.form_data_table_name, @@ -44,6 +45,8 @@ begin json_schema_name => fc.json_schema_name, new_form_data => fc.new_form_data ); + update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + elsif fc.operation = 'update' then -- store the pending project revisions corresponding form_change, and its parent select * into pending_form_change from cif.form_change @@ -82,6 +85,8 @@ begin and form_data_table_name = fc.form_data_table_name and form_data_record_id = fc.form_data_record_id; end if; + -- Set the previous_form_change_id to be the committing form change. + update cif.form_change set previous_form_change_id = fc.id where id = pending_form_change.id; end if; return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); end; From a16a83879e3739f376f068ec892249efb9705101 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 30 Nov 2023 15:14:02 -0800 Subject: [PATCH 22/51] test: check previous_form_change_id and form_data_record_id on create operations --- .../commit_form_change_internal_test.sql | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index ce82afce72..1bfb5e4d99 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(15); +select plan(18); /** SETUP **/ truncate cif.form_change restart identity; @@ -136,6 +136,13 @@ select is ( 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' ); +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 3::int, + 'When committing, the pending form change gets the committing form change as its previous form change' +); + + select is ( (select project_name from cif.project where id = 1), 'Incorrect', @@ -154,6 +161,18 @@ select is ( 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' ); +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + 4::int, + 'When committing has an operation of create, the pending form change gets the committing form change as its previous form change' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + 1::int, + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change' +); + select is ( (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), 1::bigint, From 5739bdfb2ce90fedbf6203ff0bd3500a6735997b Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 20:30:12 -0800 Subject: [PATCH 23/51] test: add separate test for checking create on contacts --- .../mutations/commit_form_change_internal_test.sql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 1bfb5e4d99..650c393d69 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(18); +select plan(19); /** SETUP **/ truncate cif.form_change restart identity; @@ -170,7 +170,13 @@ select is ( select is ( (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), 1::int, - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change' + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + 1::int, + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' ); select is ( From df054fb7351b94ffdd319fb7178af06b10a0951e Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 20:32:58 -0800 Subject: [PATCH 24/51] chore: handle the various cases for creating form changes in concurrent revisions, untested --- .../mutations/commit_form_change_internal.sql | 152 ++++++++++++++++-- 1 file changed, 139 insertions(+), 13 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index fa5ed6649f..2aac33d667 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -34,24 +34,151 @@ begin where id = fc.id; if pending_project_revision_id is not null then - -- If the committing form change is a create, then it needs to be created in the pending revision with an update operation. + /** + project_contact + project_manager + project_summary_report + funding_parameter_EP + operator (don't belong to project revisinos and thus aren't affected by concurrency) + milestone + project_attachment + funding_parameter_IA + project (create doesn't apply, update is handled.) + reporting_requirement + contact (don't belong to project revisinos and thus aren't affected by concurrency) + emission_intensity + */ + if fc.operation = 'create' then - select id into new_fc_in_pending_id from cif.create_form_change( - operation => 'update'::cif.form_change_operation, - form_data_schema_name => 'cif', - form_data_table_name => fc.form_data_table_name, - form_data_record_id => fc.form_data_record_id, - project_revision_id => pending_project_revision_id, - json_schema_name => fc.json_schema_name, - new_form_data => fc.new_form_data - ); - update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + -- These are the forms that a project can have at most one of. + -- If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. + if ( + (fc.json_schema_name in ('funding_parameter_EP', 'funding_parameter_IA', 'emission_intensity', 'project_summary_report')) and + ((select count(id) from cif.form_change where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name) > 0) + ) then + -- MIKE consider updating null fields in pending with values from committing. + update cif.form_change set + previous_form_change_id = fc.id, + form_data_record_id = recordId + where project_revision_id = pending_project_revision_id + and json_schema_name = fc.json_schema_name; + elsif ( + fc.json_schema_name = 'project_contact' + and (select count(id) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name=fc.json_schema_name) > 0 + ) then + -- if pending has any contacts, then create the new form change in pending and update the contactIndex to be the highest + -- contactIndex in pending + 1. If it doesn't then the catch-all for 'create' will handle it + select id into new_fc_in_pending_id from cif.create_form_change( + operation => 'update'::cif.form_change_operation, + form_data_schema_name => 'cif', + form_data_table_name => fc.form_data_table_name, + form_data_record_id => recordId, + project_revision_id => pending_project_revision_id, + json_schema_name => fc.json_schema_name, + new_form_data => (fc.new_form_data || format('{"contactIndex": %s}', + (select max(new_form_data ->> contactIndex) from cif.form_change + where project_revision_id=pending_project_revision_id + and json_schema_name = fc.json_schema_name + ) + 1) + ) + ); + update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + + elsif ( + fc.json_schema_name = 'project_manager' + and ( + (select count(id) from cif.form_change + where project_revision_id=pending_project_revision_id + and json_schema_name=fc.json_schema_name + and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId') > 0 + ) + ) then + -- If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id. + -- If not, then the catch all case will handle it. + update cif.form_change set + previous_form_change_id = fc.id, + form_data_record_id = recordId + where id = (select id from cif.form_change + where project_revision_id=pending_project_revision_id + and json_schema_name=fc.json_schema_name + and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId'); + + elsif ( + fc.json_schema_name = 'reporting_requirement' + and ( + (select count(id) 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') + > 0) + ) then + -- If reporting_requirements of this reportType already exist in pending, create the new form_change and set the reportingRequirementIndex + -- to the highest existing in pending plus 1. If committing is the first reporting_requirement of this reportType, then the catch-all works. + select id into new_fc_in_pending_id from cif.create_form_change( + operation => 'update'::cif.form_change_operation, + form_data_schema_name => 'cif', + form_data_table_name => fc.form_data_table_name, + form_data_record_id => recordId, + 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) 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' + ) + 1) + ) + ); + update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + + elsif ( + fc.json_schema_name = 'milestone' + and ( + (select count(id) from cif.form_change where project_revision_id = pending_project_revision_id + and json_schema_name = fc.json_schema_name) + > 0) + ) then + -- If committing is creating a milestone and milestones exist, create the milestone in pending and set the reportingRequirementIndex + -- to be the max existing in pending plus 1. If pending has no milestones, the catch-all works. + select id into new_fc_in_pending_id from cif.create_form_change( + operation => 'update'::cif.form_change_operation, + form_data_schema_name => 'cif', + form_data_table_name => fc.form_data_table_name, + form_data_record_id => recordId, + 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) from cif.form_change + where project_revision_id=pending_project_revision_id + and json_schema_name = fc.json_schema_name + ) + 1) + ) + ); + update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + /** + This next case acts as the catch-all for 'create'. It applies to any scenario in which the pending revision does not have an equivalent + form change to the one being committed. This includes unordered lists (e.g. attachments, milestones, etc.), the unique forms + like funding_parameter when pending has not created one yet, and ordered/pseudo ordered lists when the equivalent index does not + exist in pending. + **/ + else + + select id into new_fc_in_pending_id from cif.create_form_change( + operation => 'update'::cif.form_change_operation, + form_data_schema_name => 'cif', + form_data_table_name => fc.form_data_table_name, + form_data_record_id => recordId, + project_revision_id => pending_project_revision_id, + json_schema_name => fc.json_schema_name, + new_form_data => fc.new_form_data + ); + update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + end if; elsif fc.operation = 'update' then -- store the pending project revisions corresponding form_change, and its parent select * into pending_form_change from cif.form_change where project_revision_id = pending_project_revision_id - and form_data_table_name = fc.form_data_table_name + and json_schema_name = fc.json_schema_name and form_data_record_id = fc.form_data_record_id limit 1; select * into parent_of_pending_form_change from cif.form_change where id = pending_form_change.previous_form_change_id limit 1; @@ -93,7 +220,6 @@ end; $$ language plpgsql volatile; grant execute on function cif_private.commit_form_change_internal(cif.form_change, int) to cif_internal, cif_external, cif_admin; - comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler.'; commit; From f26b39381882b35adcad67936c9c75f2eb5558a0 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 20:34:31 -0800 Subject: [PATCH 25/51] chore: remove unnecessary comment --- .../mutations/commit_form_change_internal.sql | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 2aac33d667..4f1a1b3994 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -34,21 +34,6 @@ begin where id = fc.id; if pending_project_revision_id is not null then - /** - project_contact - project_manager - project_summary_report - funding_parameter_EP - operator (don't belong to project revisinos and thus aren't affected by concurrency) - milestone - project_attachment - funding_parameter_IA - project (create doesn't apply, update is handled.) - reporting_requirement - contact (don't belong to project revisinos and thus aren't affected by concurrency) - emission_intensity - */ - if fc.operation = 'create' then -- These are the forms that a project can have at most one of. -- If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. From d7f47ed2a4318b216a8071823a688ea26b54c084 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 21:43:50 -0800 Subject: [PATCH 26/51] test: cleanup assertions and add testing for creating a funding agreement --- .../commit_form_change_internal_test.sql | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 650c393d69..a482b8f3a4 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(19); +select plan(21); /** SETUP **/ truncate cif.form_change restart identity; @@ -126,10 +126,29 @@ update cif.form_change set new_form_data='{ select cif.add_contact_to_revision(3, 1, 1); select cif.add_project_attachment_to_revision(3,1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 50, + 'holdbackPercentage', 10, + 'maxFundingAmount', 1, + 'anticipatedFundingAmount', 1, + 'proponentCost',777, + 'contractStartDate', '2022-03-01 16:21:42.693489-07', + 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' + )::jsonb, + null, + 3 + ); select cif.commit_project_revision(3); --- Both committing and pending project revisions have made changes to the project form. +-- Test when both committing and pending project revisions have made changes to the project form, +-- and creates of new records in committing that do not exist in pending yet. select is ( (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), 'Correct', @@ -169,13 +188,13 @@ select is ( select is ( (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - 1::int, + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment'), 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' ); select is ( (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - 1::int, + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_contact'), 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' ); @@ -185,6 +204,18 @@ select is ( 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' ); +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' +); + +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select new_form_data from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' +); + -- Commit the ammednment select cif.commit_project_revision(2); @@ -198,7 +229,8 @@ select results_eq ( 'After committing the pending form change, the project table has all of the correct values' ); --- Test when committing has made changes to the form but the pending has not +-- Test when committing has made changes to the form but the pending has not, +-- and deleting a project attachment in the committing form change select cif.create_project_revision(1, 'Amendment'); -- id = 4 select cif.create_project_revision(1, 'General Revision'); -- id = 5 update cif.form_change set new_form_data='{ @@ -217,7 +249,7 @@ select cif.discard_project_attachment_form_change((select id from cif.form_chang select cif.commit_project_revision(5); select is ( - (select new_form_data->>'projectName' from cif.form_change where id = 8), + (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project'), 'Correct only newer', 'The pending form change should have the value from the committing form change' ); From 8ecb31284efb303f8c31aca8bd2af50ac633a803 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 22:50:58 -0800 Subject: [PATCH 27/51] chore: cast incremented indices in form data back to jsonb --- .../mutations/commit_form_change_internal.sql | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 4f1a1b3994..4516ba15fb 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -41,7 +41,6 @@ begin (fc.json_schema_name in ('funding_parameter_EP', 'funding_parameter_IA', 'emission_intensity', 'project_summary_report')) and ((select count(id) from cif.form_change where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name) > 0) ) then - -- MIKE consider updating null fields in pending with values from committing. update cif.form_change set previous_form_change_id = fc.id, form_data_record_id = recordId @@ -61,10 +60,10 @@ begin project_revision_id => pending_project_revision_id, json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"contactIndex": %s}', - (select max(new_form_data ->> contactIndex) from cif.form_change + (select max(new_form_data ->> 'contactIndex')::int from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name = fc.json_schema_name - ) + 1) + ) + 1)::jsonb ) ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; @@ -106,11 +105,11 @@ 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) 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' - ) + 1) + ) + 1)::jsonb ) ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; @@ -132,10 +131,10 @@ 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) 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) + ) + 1)::jsonb ) ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; From 34cada48aa1000eba455f5d7e3e4db76a5626049 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Dec 2023 22:51:53 -0800 Subject: [PATCH 28/51] test: prep for the restof the create tests --- .../commit_form_change_internal_test.sql | 183 +++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index a482b8f3a4..67b6161a48 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -99,7 +99,7 @@ update cif.form_change set new_form_data='{ and form_data_table_name='project'; select cif.commit_project_revision(1); - +-- create the amendment that will be "pending" select cif.create_project_revision(1, 'Amendment'); -- id = 2 update cif.form_change set new_form_data='{ "projectName": "Correct", @@ -112,6 +112,7 @@ update cif.form_change set new_form_data='{ where project_revision_id=2 and form_data_table_name='project'; +-- create the general revision that will be "committing" select cif.create_project_revision(1, 'General Revision'); -- id = 3 update cif.form_change set new_form_data='{ "projectName": "Incorrect", @@ -267,6 +268,186 @@ select is ( 'The project table should have the updated proejct name, even after the pending amendment is committed' ); +-- Test when committing is creating records of types that already exist in pending +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); + +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); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +-- Add necessary form changes for tests +select cif.add_contact_to_revision(2, 1, 1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 2 +); +select cif.add_project_attachment_to_revision(2,1); + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +-- Add necessary form changes for tests +select cif.add_contact_to_revision(3, 1, 2); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 2 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 2, + 'cifUserId', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 2 + )::jsonb, + null, + 3 +); +select cif.add_project_attachment_to_revision(3,1); +select cif.add_project_attachment_to_revision(3,2); + +select cif.commit_project_revision(3); + +-- emission_intensity +-- project_contact +-- project_manager projectManagerLabelId 1 is update and 2 is created in pending +-- reporting_requirement should be 3 total with 1,2,3 for reportingRequirementIndex +-- milestone should be 3 total with 1,2,3 for reportingRequirementIndex +-- attachment + select finish(); rollback; From 6ad612d6941b4429ba122593e97dd47a096e9317 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Sun, 10 Dec 2023 09:15:53 -0800 Subject: [PATCH 29/51] test: add contacts to use as managers --- .../unit/mutations/commit_form_change_internal_test.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 67b6161a48..adcc58e181 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -274,7 +274,13 @@ 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", From e1152b0f7cb5125e09d3df03d2ac4a8fee60927c Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Mar 2024 15:06:13 -0800 Subject: [PATCH 30/51] docs: add start of concurrent revision handling documentation --- docs/concurrentRevisionHandling.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/concurrentRevisionHandling.md diff --git a/docs/concurrentRevisionHandling.md b/docs/concurrentRevisionHandling.md new file mode 100644 index 0000000000..916fbc1529 --- /dev/null +++ b/docs/concurrentRevisionHandling.md @@ -0,0 +1,25 @@ +# Handling of Concurrent Revisions + +The purpose of this document is to outline how we allow concurrent revisions to be made to a proeject. + +## Introduction + +By "concurrent revisions" we are referring to the ability for two discrete sets of changes to a project to exist at the same time. In CIF, we limit the number of concurrent revisions on any given project to be two: one "Amendment", and one "General Revision". While this was never intended to be a functionality of CIF, a new user flow was introduced that required it. As such, the approach taken is more of an adaptaion of the existing CIF architecture (which is described in `docs/dbRecordsHistory.md`) than an architecture designed to handle concurrency. The result is that any divergence from the original architecture pre-concureency also appears in the concurrent behaviour. For example, project contacts are handled using a different pattern than the project form in the original architecture, therfore they behave differently from the genral pattern in the concurrent approach as well. + +### Terminology + +There are three terms we need to use to identify the three `form_change` records in question: "committing", "pending", and "original parent". I'll use the more common scenario to outline the terminology used throughout this document. +An Amendment is opened on a project, and left open while it is being negotiated. While it is open, a General Revision is opened on that same project, a small change is made, and the revision is committed. The point in time of the General Revision being committed is where the terminology gets its roots. In this example, the General Revision is **committing**, the still-open amendment is **pending**, and the parent revision of the Amendment is the **original parent**. + +## Approach + +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 using in-line in the `commit_form_change_internal` where they have more context. From d99a18ef5220746e7e675b002d47bc9f0a7e7ceb Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 6 Mar 2024 19:23:15 -0800 Subject: [PATCH 31/51] docs: update concurrent revision doc --- docs/concurrentRevisionHandling.md | 19 +++++++++++++++++-- .../mutations/commit_form_change_internal.sql | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/concurrentRevisionHandling.md b/docs/concurrentRevisionHandling.md index 916fbc1529..4839bce7c5 100644 --- a/docs/concurrentRevisionHandling.md +++ b/docs/concurrentRevisionHandling.md @@ -16,10 +16,25 @@ 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 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`. + +### Create (the general approach) + +If the committing project revision creates a form change that does not exist in the pending revision, for example adding a milestone, then the form needs to be created in the pending revision. In cases such as contacts which have a `contactIndex` associated with them, the index needs to be determined by the existing indices in the pending revision. This will allow the indices to stay sequential if other items were added or removed in the pending revision. + +### Update + +1. If the committing form change contains the same data as the pending's original parent, then no change to the pending data is needed. +2. If the committing and pending form changes both have changes from the pending form change's parent, then set the pending form change's new_form_data to be the committing form change's, plus the changes made in the penging form change. The result is what would the data would have been if the pending form change had the committing as it's parent, similar to a git rebase. +3. If the pending form change hasn't made any changes since its creation, but the committing form change has, set the pending form change's new_form_data to be the committing form change's, as it is the latest information. + +### Archive (the general approach) + +If the committing `form_change` is being archived, the pending form change can simply be deleted as it never would have been created in the first place had the committing project revision been the original parent of the pending revision. diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 4516ba15fb..cfd019f44d 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -175,14 +175,14 @@ begin if committing_minus_pendings_parent is not null then if pending_minus_pendings_parent is not null then -- if the committing and pending form changes both have changes from the pending form change's parent, - -- then set the pending form change to be the committing form change, plus the changes made in the penging form change. + -- then set the pending form change's new_form_data to be the committing form change's, plus the changes made in the penging form change. update cif.form_change set new_form_data = (fc.new_form_data || pending_minus_pendings_parent) where id = pending_form_change.id; else -- The pending form change hasn't made any changes since its creation, but the committing form change has. - -- Set the pending form change ot be the committing form change as it is the latest information + -- Set the pending form change's new_form_data to be the committing form change's, as it is the latest information update cif.form_change set new_form_data = (fc.new_form_data) From 68b0b9396170fa53494a3fcdbbd76818bee8d524 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Tue, 12 Mar 2024 17:36:35 -0700 Subject: [PATCH 32/51] chore: address PR comments --- .../deploy/mutations/commit_form_change_internal.sql | 11 ++++++++--- schema/test/unit/functions/jsonb_minus_test.sql | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index cfd019f44d..d686c1a7e4 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -1,6 +1,11 @@ -- Deploy cif:mutations/commit_form_change to pg begin; +/* +To allow for pending project revisions to remain up to date with the project when other revisions are commit, a significant amount +of conditional logic has been introduced in the form of nested if statements. This should be refactored to a more maintainable & +readable structure. +*/ -- We need to explicitly drop the old function here since we're changing the signature. drop function if exists cif_private.commit_form_change_internal(cif.form_change); create or replace function cif_private.commit_form_change_internal(fc cif.form_change, pending_project_revision_id int default null) @@ -60,7 +65,7 @@ begin project_revision_id => pending_project_revision_id, json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"contactIndex": %s}', - (select max(new_form_data ->> 'contactIndex')::int from cif.form_change + (select max((new_form_data ->> 'contactIndex')::int) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name = fc.json_schema_name ) + 1)::jsonb @@ -105,7 +110,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' @@ -131,7 +136,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/functions/jsonb_minus_test.sql b/schema/test/unit/functions/jsonb_minus_test.sql index 9abb42787d..37ad6a010e 100644 --- a/schema/test/unit/functions/jsonb_minus_test.sql +++ b/schema/test/unit/functions/jsonb_minus_test.sql @@ -1,6 +1,6 @@ begin; -select plan(4); +select plan(5); select has_function('cif', 'jsonb_minus', 'function cif.jsonb_minus exists'); @@ -19,7 +19,13 @@ select is( select is( (select * from cif.jsonb_minus('{"a": 1, "b": 0, "c": 1, "d": null}'::jsonb, '{"a": 0, "b": 0}'::jsonb)), '{"a": 1, "c": 1, "d": null}'::jsonb, - 'Added fields are included in the retuen value, including when their value is null' + 'Added fields are included in the return value, including when their value is null' +); + +select is( + (select * from cif.jsonb_minus('{"a": 1, "b": 0}'::jsonb, '{"a": 1, "b": 2, "d": "test value"}'::jsonb)), + '{"b": 0}'::jsonb, + 'Extra fields in the subtrahend are properly ignored' ); select finish(); From ee1108e32d94f16fa756c007b63332eddc298fe9 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 13:01:28 -0700 Subject: [PATCH 33/51] chore: set the operation of pending form changes to update when committing creates them --- schema/deploy/mutations/commit_form_change_internal.sql | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index d686c1a7e4..dd8b37e963 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -76,17 +76,18 @@ begin elsif ( fc.json_schema_name = 'project_manager' and ( - (select count(id) from cif.form_change + (select count(*) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name=fc.json_schema_name and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId') > 0 ) ) then - -- If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id. - -- If not, then the catch all case will handle it. + -- If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, + -- and the operation to 'update' to handle when pending has an opertion of 'create'. If not, then the catch all case will handle it. update cif.form_change set previous_form_change_id = fc.id, - form_data_record_id = recordId + form_data_record_id = recordId, + operation = 'update'::cif.form_change_operation where id = (select id from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name=fc.json_schema_name From 15e9846590cb35aa522f904cc535636cc43d72e4 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 13:20:47 -0700 Subject: [PATCH 34/51] test: add tests for adding project_managers with pending revision --- .../commit_form_change_internal_test.sql | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index adcc58e181..abb086cf0b 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(21); +select plan(24); /** SETUP **/ truncate cif.form_change restart identity; @@ -373,7 +373,7 @@ select cif.create_form_change( 'project_manager', json_build_object( 'projectManagerLabelId', 1, - 'cifUserId', 1, + 'cifUserId', 2, 'projectId', 1 )::jsonb, null, @@ -386,7 +386,7 @@ select cif.create_form_change( 'project_manager', json_build_object( 'projectManagerLabelId', 2, - 'cifUserId', 2, + 'cifUserId', 1, 'projectId', 1 )::jsonb, null, @@ -449,7 +449,24 @@ select cif.commit_project_revision(3); -- emission_intensity -- project_contact --- project_manager projectManagerLabelId 1 is update and 2 is created in pending +-- project_manager +select is ( + (select operation from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 'update', + 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' +); + +select is ( + (select (new_form_data->>'cifUserId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 1, + 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), + 2::bigint, + 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' +); -- reporting_requirement should be 3 total with 1,2,3 for reportingRequirementIndex -- milestone should be 3 total with 1,2,3 for reportingRequirementIndex -- attachment From fd25539f4df2732520667e77be9a77f0f796b3ae Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 15:24:01 -0700 Subject: [PATCH 35/51] test: tests for adding reporting requirements in both committing and pending form changes --- .../commit_form_change_internal_test.sql | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index abb086cf0b..182d68c707 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(24); +select plan(30); /** SETUP **/ truncate cif.form_change restart identity; @@ -467,8 +467,45 @@ select is ( 2::bigint, 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' ); --- reporting_requirement should be 3 total with 1,2,3 for reportingRequirementIndex --- milestone should be 3 total with 1,2,3 for reportingRequirementIndex + +-- Quarterly Reports +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3::bigint, + 'When committing and pending both create Quarterly reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create Quarterly reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3, + 'When committing and pending both create Quarterly reports, the indexes of those in pending are adjusted on commit' +); + +-- Milestones +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3::bigint, + 'When committing and pending both create General Milestone reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create General Milestone reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3, + 'When committing and pending both create General Milestone reports, the indexes of those in pending are adjusted on commit' +); + -- attachment select finish(); From 90550cf31393d4c4b71501441d77fb5b99d2cbd5 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 15:47:44 -0700 Subject: [PATCH 36/51] chore: fix update previous_form_change_id scope in commit_form_change_internal --- schema/deploy/mutations/commit_form_change_internal.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index dd8b37e963..496ac6f4e7 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -171,7 +171,7 @@ begin and json_schema_name = fc.json_schema_name and form_data_record_id = fc.form_data_record_id limit 1; select * into parent_of_pending_form_change from cif.form_change - where id = pending_form_change.previous_form_change_id limit 1; + where id = pending_form_change.previous_form_change_id; select (cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) into pending_minus_pendings_parent; @@ -195,6 +195,8 @@ begin where id = pending_form_change.id; end if; end if; + -- Set the previous_form_change_id to be the committing form change. + update cif.form_change set previous_form_change_id = fc.id where id = pending_form_change.id; elsif fc.operation = 'archive' then delete from cif.form_change @@ -202,8 +204,6 @@ begin and form_data_table_name = fc.form_data_table_name and form_data_record_id = fc.form_data_record_id; end if; - -- Set the previous_form_change_id to be the committing form change. - update cif.form_change set previous_form_change_id = fc.id where id = pending_form_change.id; end if; return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); end; From ef243d849390ada41f4045f2d5eb900013e087be Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 17:00:32 -0700 Subject: [PATCH 37/51] chore: remove redundant limit 1 --- schema/deploy/mutations/commit_form_change.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/schema/deploy/mutations/commit_form_change.sql b/schema/deploy/mutations/commit_form_change.sql index 1639040bea..30a9eb0626 100644 --- a/schema/deploy/mutations/commit_form_change.sql +++ b/schema/deploy/mutations/commit_form_change.sql @@ -14,9 +14,10 @@ begin return (select cif_private.commit_form_change_internal( (select row(form_change.*)::cif.form_change from cif.form_change where id = row_id), + -- This is guaranteed to be a single row as we have unique inidices on pending general revision and pending amendment (select id from cif.project_revision where project_id=(select project_id from cif.project_revision where id = form_change_patch.project_revision_id) - and change_status = 'pending' and id != form_change_patch.project_revision_id limit 1) + and change_status = 'pending' and id != form_change_patch.project_revision_id) )); end; $$ language plpgsql volatile; From f9d89474434a11a2b07e9155def7b2a47d223902 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 17:09:32 -0700 Subject: [PATCH 38/51] chore: added comments for clarity --- schema/deploy/mutations/commit_form_change_internal.sql | 2 +- schema/deploy/mutations/commit_project_revision.sql | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 496ac6f4e7..c3b7e7eb77 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -210,6 +210,6 @@ end; $$ language plpgsql volatile; grant execute on function cif_private.commit_form_change_internal(cif.form_change, int) to cif_internal, cif_external, cif_admin; -comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler.'; +comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler. Then, update a potential existing revision to include the changes that were just committed.'; commit; diff --git a/schema/deploy/mutations/commit_project_revision.sql b/schema/deploy/mutations/commit_project_revision.sql index 476ef710dc..2eac2084a2 100644 --- a/schema/deploy/mutations/commit_project_revision.sql +++ b/schema/deploy/mutations/commit_project_revision.sql @@ -11,6 +11,7 @@ begin -- defer FK constraints check to the end of the transaction set constraints all deferred; + -- Find a potential existing other pending revision that needs updating while we commit this one, that we pass as a reference for the internal commit functions select form_data_record_id into proj_id from cif.form_change where form_data_table_name='project' and project_revision_id=$1; select id into pending_project_revision_id from cif.project_revision where project_id = proj_id From bf476bf90b7f01197d0683454db506a215f67651 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 13 Mar 2024 17:13:47 -0700 Subject: [PATCH 39/51] chore: updated function comment to be more accurate --- schema/deploy/mutations/commit_form_change_internal.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index c3b7e7eb77..0ac163fcec 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -210,6 +210,6 @@ end; $$ language plpgsql volatile; grant execute on function cif_private.commit_form_change_internal(cif.form_change, int) to cif_internal, cif_external, cif_admin; -comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler. Then, update a potential existing revision to include the changes that were just committed.'; +comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler. Then, update a potential existing revision to include the changes that were just committed.'; commit; From c728566d703c69c16891fd18c7c09abbda718ee4 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Fri, 15 Mar 2024 12:02:46 -0700 Subject: [PATCH 40/51] test: update the milestone concurrent revision tests --- .../mutations/commit_form_change_internal.sql | 3 ++- .../mutations/commit_form_change_internal_test.sql | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index 0ac163fcec..d24eac4077 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -210,6 +210,7 @@ end; $$ language plpgsql volatile; grant execute on function cif_private.commit_form_change_internal(cif.form_change, int) to cif_internal, cif_external, cif_admin; -comment on function cif_private.commit_form_change_internal(cif.form_change, int) is 'Commits the form change and calls the corresponding commit handler. Then, update a potential existing revision to include the changes that were just committed.'; +comment on function cif_private.commit_form_change_internal(cif.form_change, int) is + 'Commits the form change and calls the corresponding commit handler. Then, update a potential existing revision to include the changes that were just committed.'; commit; diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 182d68c707..80e26e1b0d 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(30); +select plan(31); /** SETUP **/ truncate cif.form_change restart identity; @@ -503,7 +503,17 @@ select is ( select is ( (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), 3, - 'When committing and pending both create General Milestone reports, the indexes of those in pending are adjusted on commit' + 'When committing and pending both create General Milestone reports, the next sequential index is assigned to the milestone form_change record being added in pending.' +); + +select is ( + (select max(counts.index_count) from ( + select count(*) as index_count from cif.form_change + where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' + group by new_form_data->>'reportingRequirementIndex') as counts + ), + 1::bigint, + 'When committing and pending both create General Milestone reports, each milestone is given a unique reportingRequirementIndex.' ); -- attachment From bb2240fb70cf30095d422b7dab122eb3244e431a Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 18 Mar 2024 16:53:16 -0700 Subject: [PATCH 41/51] test: move concurrent revision tests to a separate file and add contact tests to them --- .../commit_form_change_internal_test.sql | 440 +---------------- .../mutations/concurrent_revisions_test.sql | 467 ++++++++++++++++++ 2 files changed, 468 insertions(+), 439 deletions(-) create mode 100644 schema/test/unit/mutations/concurrent_revisions_test.sql diff --git a/schema/test/unit/mutations/commit_form_change_internal_test.sql b/schema/test/unit/mutations/commit_form_change_internal_test.sql index 80e26e1b0d..13fa18ae0e 100644 --- a/schema/test/unit/mutations/commit_form_change_internal_test.sql +++ b/schema/test/unit/mutations/commit_form_change_internal_test.sql @@ -1,6 +1,6 @@ begin; -select plan(31); +select plan(6); /** SETUP **/ truncate cif.form_change restart identity; @@ -78,445 +78,7 @@ select is( ); --- Test the concurrent revision functinality -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); - --- create the amendment that will be "pending" -select cif.create_project_revision(1, 'Amendment'); -- id = 2 -update cif.form_change set new_form_data='{ - "projectName": "Correct", - "summary": "original (incorrect at point of test)", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=2 - and form_data_table_name='project'; - --- create the general revision that will be "committing" -select cif.create_project_revision(1, 'General Revision'); -- id = 3 -update cif.form_change set new_form_data='{ - "projectName": "Incorrect", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=3 - and form_data_table_name='project'; - -select cif.add_contact_to_revision(3, 1, 1); -select cif.add_project_attachment_to_revision(3,1); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 50, - 'holdbackPercentage', 10, - 'maxFundingAmount', 1, - 'anticipatedFundingAmount', 1, - 'proponentCost',777, - 'contractStartDate', '2022-03-01 16:21:42.693489-07', - 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' - )::jsonb, - null, - 3 - ); - -select cif.commit_project_revision(3); - --- Test when both committing and pending project revisions have made changes to the project form, --- and creates of new records in committing that do not exist in pending yet. -select is ( - (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 'Correct', - 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' -); - -select is ( - (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 3::int, - 'When committing, the pending form change gets the committing form change as its previous form change' -); - - -select is ( - (select project_name from cif.project where id = 1), - 'Incorrect', - 'The project receives the value from the committing form change' -); - -select is ( - (select new_form_data->>'summary' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 'Correct', - 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' -); - -select is ( - (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, - 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' -); - -select is ( - (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - 4::int, - 'When committing has an operation of create, the pending form change gets the committing form change as its previous form change' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_contact'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - 1::bigint, - 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' -); - -select is ( - (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), - (select new_form_data from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' -); - --- Commit the ammednment -select cif.commit_project_revision(2); - -select results_eq ( - $$ - (select project_name, summary, funding_stream_rfp_id, project_status_id, proposal_reference, operator_id from cif.project where id = 1) - $$, - $$ - values('Correct'::varchar, 'Correct'::varchar, 1::int, 1::int, '1235'::varchar, 1::int) - $$, - 'After committing the pending form change, the project table has all of the correct values' -); - --- Test when committing has made changes to the form but the pending has not, --- and deleting a project attachment in the committing form change -select cif.create_project_revision(1, 'Amendment'); -- id = 4 -select cif.create_project_revision(1, 'General Revision'); -- id = 5 -update cif.form_change set new_form_data='{ - "projectName": "Correct only newer", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=5 - and form_data_table_name='project'; - -select cif.discard_project_attachment_form_change((select id from cif.form_change where project_revision_id = 5 and form_data_table_name = 'project_attachment')); - -select cif.commit_project_revision(5); - -select is ( - (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project'), - 'Correct only newer', - 'The pending form change should have the value from the committing form change' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project_attachment'), - 0::bigint, - 'When the committing form change is discarding a project attachment, the pending fc is deleted.' -); - -select cif.commit_project_revision(4); -select is ( - (select project_name from cif.project where id = 1), - 'Correct only newer', - 'The project table should have the updated proejct name, even after the pending amendment is committed' -); - --- Test when committing is creating records of types that already exist in pending -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.commit_project_revision(1); - --- create the amendment that will be "pending" -select cif.create_project_revision(1, 'Amendment'); -- id = 2 --- Add necessary form changes for tests -select cif.add_contact_to_revision(2, 1, 1); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 1, - 'cifUserId', 1, - 'projectId', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 1, - 'projectId', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 1 - )::jsonb, - null, - 2 -); -select cif.add_project_attachment_to_revision(2,1); - --- create the general revision that will be "committing" -select cif.create_project_revision(1, 'General Revision'); -- id = 3 --- Add necessary form changes for tests -select cif.add_contact_to_revision(3, 1, 2); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 2 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 1, - 'cifUserId', 2, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 2, - 'cifUserId', 1, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 1, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 2, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 2 - )::jsonb, - null, - 3 -); -select cif.add_project_attachment_to_revision(3,1); -select cif.add_project_attachment_to_revision(3,2); - -select cif.commit_project_revision(3); - --- emission_intensity --- project_contact --- project_manager -select is ( - (select operation from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), - 'update', - 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' -); - -select is ( - (select (new_form_data->>'cifUserId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), - 1, - 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), - 2::bigint, - 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' -); - --- Quarterly Reports -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), - 3::bigint, - 'When committing and pending both create Quarterly reports, all of them are kept after the commit' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly' and (new_form_data->>'reportingRequirementIndex')::int = 1), - 1::bigint, - 'When committing and pending both create Quarterly reports, reportingRequirementIndexes are not doubled up' -); - -select is ( - (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), - 3, - 'When committing and pending both create Quarterly reports, the indexes of those in pending are adjusted on commit' -); - --- Milestones -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), - 3::bigint, - 'When committing and pending both create General Milestone reports, all of them are kept after the commit' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' and (new_form_data->>'reportingRequirementIndex')::int = 1), - 1::bigint, - 'When committing and pending both create General Milestone reports, reportingRequirementIndexes are not doubled up' -); - -select is ( - (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), - 3, - 'When committing and pending both create General Milestone reports, the next sequential index is assigned to the milestone form_change record being added in pending.' -); - -select is ( - (select max(counts.index_count) from ( - select count(*) as index_count from cif.form_change - where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' - group by new_form_data->>'reportingRequirementIndex') as counts - ), - 1::bigint, - 'When committing and pending both create General Milestone reports, each milestone is given a unique reportingRequirementIndex.' -); - --- attachment select finish(); diff --git a/schema/test/unit/mutations/concurrent_revisions_test.sql b/schema/test/unit/mutations/concurrent_revisions_test.sql new file mode 100644 index 0000000000..4040b68179 --- /dev/null +++ b/schema/test/unit/mutations/concurrent_revisions_test.sql @@ -0,0 +1,467 @@ +begin; + +select plan(28); + +-- Test the concurrent revision functinality + +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); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +update cif.form_change set new_form_data='{ + "projectName": "Correct", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=2 + and form_data_table_name='project'; + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +update cif.form_change set new_form_data='{ + "projectName": "Incorrect", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=3 + and form_data_table_name='project'; + +select cif.add_contact_to_revision(3, 1, 1); +select cif.add_project_attachment_to_revision(3,1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 50, + 'holdbackPercentage', 10, + 'maxFundingAmount', 1, + 'anticipatedFundingAmount', 1, + 'proponentCost',777, + 'contractStartDate', '2022-03-01 16:21:42.693489-07', + 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' + )::jsonb, + null, + 3 + ); + +select cif.commit_project_revision(3); + +-- Test when both committing and pending project revisions have made changes to the project form, +-- and creates of new records in committing that do not exist in pending yet. +select is ( + (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' +); + +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 3::int, + 'When committing, the pending form change gets the committing form change as its previous form change' +); + + +select is ( + (select project_name from cif.project where id = 1), + 'Incorrect', + 'The project receives the value from the committing form change' +); + +select is ( + (select new_form_data->>'summary' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' +); + +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, + 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' +); + +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + 4::int, + 'When committing has an operation of create, the pending form change gets the committing form change as its previous form change' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_contact'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + 1::bigint, + 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' +); + +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select new_form_data from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' +); + +-- Commit the ammednment +select cif.commit_project_revision(2); + +select results_eq ( + $$ + (select project_name, summary, funding_stream_rfp_id, project_status_id, proposal_reference, operator_id from cif.project where id = 1) + $$, + $$ + values('Correct'::varchar, 'Correct'::varchar, 1::int, 1::int, '1235'::varchar, 1::int) + $$, + 'After committing the pending form change, the project table has all of the correct values' +); + +-- Test when committing has made changes to the form but the pending has not, +-- and deleting a project attachment in the committing form change +select cif.create_project_revision(1, 'Amendment'); -- id = 4 +select cif.create_project_revision(1, 'General Revision'); -- id = 5 +update cif.form_change set new_form_data='{ + "projectName": "Correct only newer", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=5 + and form_data_table_name='project'; + +select cif.discard_project_attachment_form_change((select id from cif.form_change where project_revision_id = 5 and form_data_table_name = 'project_attachment')); + +select cif.commit_project_revision(5); + +select is ( + (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project'), + 'Correct only newer', + 'The pending form change should have the value from the committing form change' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project_attachment'), + 0::bigint, + 'When the committing form change is discarding a project attachment, the pending fc is deleted.' +); + +select cif.commit_project_revision(4); +select is ( + (select project_name from cif.project where id = 1), + 'Correct only newer', + 'The project table should have the updated proejct name, even after the pending amendment is committed' +); + +-- Test when committing is creating records of types that already exist in pending +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.commit_project_revision(1); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +-- Add necessary form changes for tests +select cif.add_contact_to_revision(2, 1, 1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 2 +); +select cif.add_project_attachment_to_revision(2,1); + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +-- Add necessary form changes for tests +select cif.add_contact_to_revision(3, 1, 2); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 2 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 2, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 2 + )::jsonb, + null, + 3 +); +select cif.add_project_attachment_to_revision(3,1); +select cif.add_project_attachment_to_revision(3,2); + +select cif.commit_project_revision(3); + +-- emission_intensity +-- project_contact +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact'), + 2::bigint, + 'When committing and pending both create a primary contact, the correct nuber of contacts exist in pending' +); + +select is ( + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '2'), + 2, + 'When committing and pending both create a primary contact, the committing primary becomes the amendments secondary contact' +); + +select is ( + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '1'), + 1, + 'When committing and pending both create a primary contact, the pending primary contact contactId maintains its value after commiting is commit' +); + +-- project_manager +select is ( + (select operation from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 'update', + 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' +); + +select is ( + (select (new_form_data->>'cifUserId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 1, + 'When committing and pending both create a project manager with the same projectManagerLabelId, the value of cifUserId in pending remains unchanged' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), + 2::bigint, + 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' +); + +-- Quarterly Reports +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3::bigint, + 'When committing and pending both create Quarterly reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create Quarterly reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3, + 'When committing and pending both create Quarterly reports, the indexes of those in pending are adjusted on commit' +); + +-- Milestones +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3::bigint, + 'When committing and pending both create General Milestone reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create General Milestone reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3, + 'When committing and pending both create General Milestone reports, the next sequential index is assigned to the milestone form_change record being added in pending.' +); + +select is ( + (select max(counts.index_count) from ( + select count(*) as index_count from cif.form_change + where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' + group by new_form_data->>'reportingRequirementIndex') as counts + ), + 1::bigint, + 'When committing and pending both create General Milestone reports, each milestone is given a unique reportingRequirementIndex.' +); + +-- funding_parameter +-- attachment + + +select finish(); + +rollback; From 3a96df514e0c6981d8f587d55d92d9b950acbfc8 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Mon, 18 Mar 2024 20:45:41 -0700 Subject: [PATCH 42/51] test: add directory of tests related to concurrent revisions and update the tests --- .../conflicting_creates_test.sql | 281 ++++++++++++++++++ .../conflicting_updates_test.sql | 111 +++++++ .../creating_new_records_test.sql | 119 ++++++++ .../discarding_attachment_test.sql | 72 +++++ 4 files changed, 583 insertions(+) create mode 100644 schema/test/unit/concurrent_revisions/conflicting_creates_test.sql create mode 100644 schema/test/unit/concurrent_revisions/conflicting_updates_test.sql create mode 100644 schema/test/unit/concurrent_revisions/creating_new_records_test.sql create mode 100644 schema/test/unit/concurrent_revisions/discarding_attachment_test.sql diff --git a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql new file mode 100644 index 0000000000..ee5e38293b --- /dev/null +++ b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql @@ -0,0 +1,281 @@ +begin; + +select plan(14); + + +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.commit_project_revision(1); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +select cif.add_contact_to_revision(2, 1, 1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 2 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 2 +); +select cif.add_project_attachment_to_revision(2,1); + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +-- Add necessary form changes for tests +select cif.add_contact_to_revision(3, 1, 2); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 2 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 1, + 'cifUserId', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'project_manager', + 'cif', + 'project_manager', + json_build_object( + 'projectManagerLabelId', 2, + 'cifUserId', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 1, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'reporting_requirement', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'Quarterly', + 'reportingRequirementIndex', 2, + 'projectId', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 1 + )::jsonb, + null, + 3 +); +select cif.create_form_change( + 'create', + 'milestone', + 'cif', + 'reporting_requirement', + json_build_object( + 'reportType', 'General Milestone', + 'reportingRequirementIndex', 2 + )::jsonb, + null, + 3 +); +select cif.add_project_attachment_to_revision(3,1); +select cif.add_project_attachment_to_revision(3,2); + +select cif.commit_project_revision(3); + +-- emission_intensity +-- project_contact +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact'), + 2::bigint, + 'When committing and pending both create a primary contact, the correct nuber of contacts exist in pending' +); + +select is ( + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '2'), + 2, + 'When committing and pending both create a primary contact, the committing primary becomes the amendments secondary contact' +); + +select is ( + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '1'), + 1, + 'When committing and pending both create a primary contact, the pending primary contact contactId maintains its value after commiting is commit' +); + +-- project_manager +select is ( + (select operation from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 'update', + 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' +); + +select is ( + (select (new_form_data->>'cifUserId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), + 1, + 'When committing and pending both create a project manager with the same projectManagerLabelId, the value of cifUserId in pending remains unchanged' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), + 2::bigint, + 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' +); + +-- Quarterly Reports +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3::bigint, + 'When committing and pending both create Quarterly reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create Quarterly reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), + 3, + 'When committing and pending both create Quarterly reports, the indexes of those in pending are adjusted on commit' +); + +-- Milestones +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3::bigint, + 'When committing and pending both create General Milestone reports, all of them are kept after the commit' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' and (new_form_data->>'reportingRequirementIndex')::int = 1), + 1::bigint, + 'When committing and pending both create General Milestone reports, reportingRequirementIndexes are not doubled up' +); + +select is ( + (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), + 3, + 'When committing and pending both create General Milestone reports, the next sequential index is assigned to the milestone form_change record being added in pending.' +); + +select is ( + (select max(counts.index_count) from ( + select count(*) as index_count from cif.form_change + where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' + group by new_form_data->>'reportingRequirementIndex') as counts + ), + 1::bigint, + 'When committing and pending both create General Milestone reports, each milestone is given a unique reportingRequirementIndex.' +); + +-- 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/conflicting_updates_test.sql b/schema/test/unit/concurrent_revisions/conflicting_updates_test.sql new file mode 100644 index 0000000000..4eea8e3f5e --- /dev/null +++ b/schema/test/unit/concurrent_revisions/conflicting_updates_test.sql @@ -0,0 +1,111 @@ +begin; + +select plan(5); + +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); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +update cif.form_change set new_form_data='{ + "projectName": "Correct", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=2 + and form_data_table_name='project'; + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +update cif.form_change set new_form_data='{ + "projectName": "Incorrect", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=3 + and form_data_table_name='project'; + +select cif.add_contact_to_revision(3, 1, 1); +select cif.add_project_attachment_to_revision(3,1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 50, + 'holdbackPercentage', 10, + 'maxFundingAmount', 1, + 'anticipatedFundingAmount', 1, + 'proponentCost',777, + 'contractStartDate', '2022-03-01 16:21:42.693489-07', + 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' + )::jsonb, + null, + 3 + ); + +select cif.commit_project_revision(3); + +select is ( + (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' +); + +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 3::int, + 'When committing, the pending form change gets the committing form change as its previous form change' +); + + +select is ( + (select project_name from cif.project where id = 1), + 'Incorrect', + 'The project receives the value from the committing form change' +); + +select is ( + (select new_form_data->>'summary' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct', + 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' +); + +-- 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/creating_new_records_test.sql b/schema/test/unit/concurrent_revisions/creating_new_records_test.sql new file mode 100644 index 0000000000..a23ebf5a55 --- /dev/null +++ b/schema/test/unit/concurrent_revisions/creating_new_records_test.sql @@ -0,0 +1,119 @@ +begin; + +select plan(8); + +/* + Test when the committing project_revision creates records that do not exist in the pending project_revision +*/ + +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); + +-- create the amendment that will be "pending" +select cif.create_project_revision(1, 'Amendment'); -- id = 2 +update cif.form_change set new_form_data='{ + "projectName": "Correct", + "summary": "original (incorrect at point of test)", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1235", + "operatorId": 1 + }'::jsonb + where project_revision_id=2 + and form_data_table_name='project'; + +-- create the general revision that will be "committing" +select cif.create_project_revision(1, 'General Revision'); -- id = 3 +select cif.add_contact_to_revision(3, 1, 1); +select cif.add_project_attachment_to_revision(3,1); +select cif.create_form_change( + 'create', + 'funding_parameter_EP', + 'cif', + 'funding_parameter', + json_build_object( + 'projectId', 1, + 'provinceSharePercentage', 50, + 'holdbackPercentage', 10, + 'maxFundingAmount', 1, + 'anticipatedFundingAmount', 1, + 'proponentCost',777, + 'contractStartDate', '2022-03-01 16:21:42.693489-07', + 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' + )::jsonb, + null, + 3 + ); + +select cif.commit_project_revision(3); + +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, + 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' +); + +select is ( + (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + 4::int, + 'When committing form change has an operation of create, the pending form change that is created gets the committing form_change id as its previous form change' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_contact'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), + 1::bigint, + 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' +); + +select is ( + (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' +); + +select is ( + (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), + (select new_form_data from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), + 'When committing has an operation of create, the new_form_data propogates to the pending form change for funding parameter form' +); + +-- 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/discarding_attachment_test.sql b/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql new file mode 100644 index 0000000000..c9937833e2 --- /dev/null +++ b/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql @@ -0,0 +1,72 @@ +begin; + +select plan(4); + +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 +update cif.form_change set new_form_data='{ + "projectName": "Correct only newer", + "summary": "Correct", + "fundingStreamRfpId": 1, + "projectStatusId": 1, + "proposalReference": "1233", + "operatorId": 1 + }'::jsonb + where project_revision_id=3 + and form_data_table_name='project'; + +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 new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), + 'Correct only newer', + 'The pending form change should have the value from the committing form change' +); + +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 project_name from cif.project where id = 1), + 'Correct only newer', + 'The project table should have the updated proejct name, even after the pending amendment is committed' +); + + + +select finish(); + +rollback; From c69066b1c9a54f04a2413f8ae6c4b10fd4a33204 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 20 Mar 2024 11:07:28 -0700 Subject: [PATCH 43/51] chore: when committing archives something that pending has updated change pendings operation to create and null the record id and previous form change --- .../mutations/commit_form_change_internal.sql | 81 ++++++++++++++----- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index d24eac4077..a67dcea48c 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -40,8 +40,10 @@ begin if pending_project_revision_id is not null then if fc.operation = 'create' then - -- These are the forms that a project can have at most one of. - -- If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. + /* + These are the forms that a project can have at most one of. + If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. + */ if ( (fc.json_schema_name in ('funding_parameter_EP', 'funding_parameter_IA', 'emission_intensity', 'project_summary_report')) and ((select count(id) from cif.form_change where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name) > 0) @@ -73,6 +75,10 @@ begin ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + /* + If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, + and the operation to 'update' to handle when pending has an opertion of 'create'. If not, then the catch all case will handle it. + */ elsif ( fc.json_schema_name = 'project_manager' and ( @@ -82,8 +88,6 @@ begin and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId') > 0 ) ) then - -- If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, - -- and the operation to 'update' to handle when pending has an opertion of 'create'. If not, then the catch all case will handle it. update cif.form_change set previous_form_change_id = fc.id, form_data_record_id = recordId, @@ -93,6 +97,10 @@ begin and json_schema_name=fc.json_schema_name and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId'); + /* + If reporting_requirements of this reportType already exist in pending, create the new form_change and set the reportingRequirementIndex + to the highest existing in pending plus 1. If committing is the first reporting_requirement of this reportType, then the catch-all works. + */ elsif ( fc.json_schema_name = 'reporting_requirement' and ( @@ -101,8 +109,6 @@ begin and new_form_data ->> 'reportType' = fc.new_form_data ->> 'reportType') > 0) ) then - -- If reporting_requirements of this reportType already exist in pending, create the new form_change and set the reportingRequirementIndex - -- to the highest existing in pending plus 1. If committing is the first reporting_requirement of this reportType, then the catch-all works. select id into new_fc_in_pending_id from cif.create_form_change( operation => 'update'::cif.form_change_operation, form_data_schema_name => 'cif', @@ -120,6 +126,11 @@ begin ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + /* + If committing is creating a milestone and milestones exist, create the milestone in pending and set the + reportingRequirementIndex to be the max existing in pending plus 1. + If pending has no milestones, the catch-all works. + */ elsif ( fc.json_schema_name = 'milestone' and ( @@ -127,8 +138,6 @@ begin and json_schema_name = fc.json_schema_name) > 0) ) then - -- If committing is creating a milestone and milestones exist, create the milestone in pending and set the reportingRequirementIndex - -- to be the max existing in pending plus 1. If pending has no milestones, the catch-all works. select id into new_fc_in_pending_id from cif.create_form_change( operation => 'update'::cif.form_change_operation, form_data_schema_name => 'cif', @@ -144,11 +153,12 @@ begin ) ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + /** - This next case acts as the catch-all for 'create'. It applies to any scenario in which the pending revision does not have an equivalent - form change to the one being committed. This includes unordered lists (e.g. attachments, milestones, etc.), the unique forms - like funding_parameter when pending has not created one yet, and ordered/pseudo ordered lists when the equivalent index does not - exist in pending. + This next case acts as the catch-all for 'create'. It applies to any scenario in which the pending revision does not have an equivalent + form change to the one being committed. This includes unordered lists (e.g. attachments, milestones, etc.), the unique forms + like funding_parameter when pending has not created one yet, and ordered/pseudo ordered lists when the equivalent index does not + exist in pending. **/ else @@ -178,17 +188,23 @@ begin select (cif.jsonb_minus(fc.new_form_data, parent_of_pending_form_change.new_form_data)) into committing_minus_pendings_parent; + /* + If the committing and pending form changes both have changes from the pending form change's parent, + then set the pending form change's new_form_data to be the committing form change's, and apply the changes + made in the pending form change to that data. + */ if committing_minus_pendings_parent is not null then if pending_minus_pendings_parent is not null then - -- if the committing and pending form changes both have changes from the pending form change's parent, - -- then set the pending form change's new_form_data to be the committing form change's, plus the changes made in the penging form change. update cif.form_change set new_form_data = (fc.new_form_data || pending_minus_pendings_parent) where id = pending_form_change.id; + + /* + If the pending form change hasn't made any changes since its creation, but the committing form change has, + set the pending form change's new_form_data to be the committing form_change's, as it is the latest information. + */ else - -- The pending form change hasn't made any changes since its creation, but the committing form change has. - -- Set the pending form change's new_form_data to be the committing form change's, as it is the latest information update cif.form_change set new_form_data = (fc.new_form_data) @@ -199,10 +215,37 @@ begin update cif.form_change set previous_form_change_id = fc.id where id = pending_form_change.id; elsif fc.operation = 'archive' then - delete from cif.form_change - where project_revision_id = pending_project_revision_id - and form_data_table_name = fc.form_data_table_name + select * into pending_form_change from cif.form_change + where project_revision_id = pending_project_revision_id + and json_schema_name = fc.json_schema_name and form_data_record_id = fc.form_data_record_id; + select * into parent_of_pending_form_change from cif.form_change + where id = pending_form_change.previous_form_change_id; + + select (cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) + into pending_minus_pendings_parent; + + /* + If pending has made changes, then set its operation to create and null the form data record id & previous form change id + since it's technically creating them now. This way the archiving still took place in the committing form change, and we + avoid trying to update the now archived record that form_data_record_id points to. + */ + if pending_minus_pendings_parent is not null then + update cif.form_change set + operation = 'create'::cif.form_change_operation, + form_data_record_id = null, + previous_form_change_id = null + where id = pending_form_change.id; + + else + /* + If pending has not made changes to the form data, the pending record can be deleted as it never would have been made + */ + delete from cif.form_change + where project_revision_id = pending_project_revision_id + and form_data_table_name = fc.form_data_table_name + and form_data_record_id = fc.form_data_record_id; + end if; end if; end if; return (select row(form_change.*)::cif.form_change from cif.form_change where id = fc.id); From 0a7101df10b3a7e6b660d183524377d7300752e4 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Wed, 20 Mar 2024 11:09:02 -0700 Subject: [PATCH 44/51] chore: remove test file that whose tests have been distributed to more specific test files --- .../mutations/concurrent_revisions_test.sql | 467 ------------------ 1 file changed, 467 deletions(-) delete mode 100644 schema/test/unit/mutations/concurrent_revisions_test.sql diff --git a/schema/test/unit/mutations/concurrent_revisions_test.sql b/schema/test/unit/mutations/concurrent_revisions_test.sql deleted file mode 100644 index 4040b68179..0000000000 --- a/schema/test/unit/mutations/concurrent_revisions_test.sql +++ /dev/null @@ -1,467 +0,0 @@ -begin; - -select plan(28); - --- Test the concurrent revision functinality - -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); - --- create the amendment that will be "pending" -select cif.create_project_revision(1, 'Amendment'); -- id = 2 -update cif.form_change set new_form_data='{ - "projectName": "Correct", - "summary": "original (incorrect at point of test)", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=2 - and form_data_table_name='project'; - --- create the general revision that will be "committing" -select cif.create_project_revision(1, 'General Revision'); -- id = 3 -update cif.form_change set new_form_data='{ - "projectName": "Incorrect", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=3 - and form_data_table_name='project'; - -select cif.add_contact_to_revision(3, 1, 1); -select cif.add_project_attachment_to_revision(3,1); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 50, - 'holdbackPercentage', 10, - 'maxFundingAmount', 1, - 'anticipatedFundingAmount', 1, - 'proponentCost',777, - 'contractStartDate', '2022-03-01 16:21:42.693489-07', - 'projectAssetsLifeEndDate', '2022-03-01 16:21:42.693489-07' - )::jsonb, - null, - 3 - ); - -select cif.commit_project_revision(3); - --- Test when both committing and pending project revisions have made changes to the project form, --- and creates of new records in committing that do not exist in pending yet. -select is ( - (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 'Correct', - 'When both the committing and pending form changes have changed the same field, the value from the pending should persist' -); - -select is ( - (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 3::int, - 'When committing, the pending form change gets the committing form change as its previous form change' -); - - -select is ( - (select project_name from cif.project where id = 1), - 'Incorrect', - 'The project receives the value from the committing form change' -); - -select is ( - (select new_form_data->>'summary' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 'Correct', - 'When the commiting form change has updated a field that the pending has not, it updates the pending form change' -); - -select is ( - (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - '{"contactId": 1, "projectId": 1, "contactIndex": 1}'::jsonb, - 'When the committing form change is creating a project contact, the contact also gets created in the pending revision' -); - -select is ( - (select previous_form_change_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - 4::int, - 'When committing has an operation of create, the pending form change gets the committing form change as its previous form change' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_attachment'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for attachments' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_contact'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'project_contact'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for contacts' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), - 1::bigint, - 'When the committing form change is creating a project attachment, the attachment also gets created in the pending revision' -); - -select is ( - (select form_data_record_id from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), - (select form_data_record_id from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' -); - -select is ( - (select new_form_data from cif.form_change where project_revision_id = 2 and form_data_table_name = 'funding_parameter_EP'), - (select new_form_data from cif.form_change where project_revision_id = 3 and form_data_table_name = 'funding_parameter_EP'), - 'When committing has an operation of create, the form_data_record_id propogates to the pending form change for funding parameter form' -); - --- Commit the ammednment -select cif.commit_project_revision(2); - -select results_eq ( - $$ - (select project_name, summary, funding_stream_rfp_id, project_status_id, proposal_reference, operator_id from cif.project where id = 1) - $$, - $$ - values('Correct'::varchar, 'Correct'::varchar, 1::int, 1::int, '1235'::varchar, 1::int) - $$, - 'After committing the pending form change, the project table has all of the correct values' -); - --- Test when committing has made changes to the form but the pending has not, --- and deleting a project attachment in the committing form change -select cif.create_project_revision(1, 'Amendment'); -- id = 4 -select cif.create_project_revision(1, 'General Revision'); -- id = 5 -update cif.form_change set new_form_data='{ - "projectName": "Correct only newer", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1235", - "operatorId": 1 - }'::jsonb - where project_revision_id=5 - and form_data_table_name='project'; - -select cif.discard_project_attachment_form_change((select id from cif.form_change where project_revision_id = 5 and form_data_table_name = 'project_attachment')); - -select cif.commit_project_revision(5); - -select is ( - (select new_form_data->>'projectName' from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project'), - 'Correct only newer', - 'The pending form change should have the value from the committing form change' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 4 and form_data_table_name = 'project_attachment'), - 0::bigint, - 'When the committing form change is discarding a project attachment, the pending fc is deleted.' -); - -select cif.commit_project_revision(4); -select is ( - (select project_name from cif.project where id = 1), - 'Correct only newer', - 'The project table should have the updated proejct name, even after the pending amendment is committed' -); - --- Test when committing is creating records of types that already exist in pending -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.commit_project_revision(1); - --- create the amendment that will be "pending" -select cif.create_project_revision(1, 'Amendment'); -- id = 2 --- Add necessary form changes for tests -select cif.add_contact_to_revision(2, 1, 1); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 1, - 'cifUserId', 1, - 'projectId', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 1, - 'projectId', 1 - )::jsonb, - null, - 2 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 1 - )::jsonb, - null, - 2 -); -select cif.add_project_attachment_to_revision(2,1); - --- create the general revision that will be "committing" -select cif.create_project_revision(1, 'General Revision'); -- id = 3 --- Add necessary form changes for tests -select cif.add_contact_to_revision(3, 1, 2); -select cif.create_form_change( - 'create', - 'funding_parameter_EP', - 'cif', - 'funding_parameter', - json_build_object( - 'projectId', 1, - 'provinceSharePercentage', 2 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 1, - 'cifUserId', 2, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'project_manager', - 'cif', - 'project_manager', - json_build_object( - 'projectManagerLabelId', 2, - 'cifUserId', 1, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 1, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'reporting_requirement', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'Quarterly', - 'reportingRequirementIndex', 2, - 'projectId', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 1 - )::jsonb, - null, - 3 -); -select cif.create_form_change( - 'create', - 'milestone', - 'cif', - 'reporting_requirement', - json_build_object( - 'reportType', 'General Milestone', - 'reportingRequirementIndex', 2 - )::jsonb, - null, - 3 -); -select cif.add_project_attachment_to_revision(3,1); -select cif.add_project_attachment_to_revision(3,2); - -select cif.commit_project_revision(3); - --- emission_intensity --- project_contact -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact'), - 2::bigint, - 'When committing and pending both create a primary contact, the correct nuber of contacts exist in pending' -); - -select is ( - (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '2'), - 2, - 'When committing and pending both create a primary contact, the committing primary becomes the amendments secondary contact' -); - -select is ( - (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '1'), - 1, - 'When committing and pending both create a primary contact, the pending primary contact contactId maintains its value after commiting is commit' -); - --- project_manager -select is ( - (select operation from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), - 'update', - 'When committing and pending both create a project manager with the same projectManagerLabelId, pendings operation becomes update' -); - -select is ( - (select (new_form_data->>'cifUserId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager' and new_form_data->>'projectManagerLabelId' = '1'), - 1, - 'When committing and pending both create a project manager with the same projectManagerLabelId, the value of cifUserId in pending remains unchanged' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_manager'), - 2::bigint, - 'When committing and pending both create a project managers with different labels, all created manager labels persist to the pending form change' -); - --- Quarterly Reports -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), - 3::bigint, - 'When committing and pending both create Quarterly reports, all of them are kept after the commit' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly' and (new_form_data->>'reportingRequirementIndex')::int = 1), - 1::bigint, - 'When committing and pending both create Quarterly reports, reportingRequirementIndexes are not doubled up' -); - -select is ( - (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'Quarterly'), - 3, - 'When committing and pending both create Quarterly reports, the indexes of those in pending are adjusted on commit' -); - --- Milestones -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), - 3::bigint, - 'When committing and pending both create General Milestone reports, all of them are kept after the commit' -); - -select is ( - (select count(*) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' and (new_form_data->>'reportingRequirementIndex')::int = 1), - 1::bigint, - 'When committing and pending both create General Milestone reports, reportingRequirementIndexes are not doubled up' -); - -select is ( - (select max((new_form_data->>'reportingRequirementIndex')::int) from cif.form_change where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone'), - 3, - 'When committing and pending both create General Milestone reports, the next sequential index is assigned to the milestone form_change record being added in pending.' -); - -select is ( - (select max(counts.index_count) from ( - select count(*) as index_count from cif.form_change - where project_revision_id = 2 and new_form_data->>'reportType' = 'General Milestone' - group by new_form_data->>'reportingRequirementIndex') as counts - ), - 1::bigint, - 'When committing and pending both create General Milestone reports, each milestone is given a unique reportingRequirementIndex.' -); - --- funding_parameter --- attachment - - -select finish(); - -rollback; From 85ce6b43571ea06f7a6a50b525bf2a4189b06d44 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 21 Mar 2024 20:02:43 -0700 Subject: [PATCH 45/51] chore: clean out unrelated items from test --- .../discarding_attachment_test.sql | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql b/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql index c9937833e2..ebe8867e20 100644 --- a/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql +++ b/schema/test/unit/concurrent_revisions/discarding_attachment_test.sql @@ -1,6 +1,6 @@ begin; -select plan(4); +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'); @@ -23,27 +23,10 @@ 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 -update cif.form_change set new_form_data='{ - "projectName": "Correct only newer", - "summary": "Correct", - "fundingStreamRfpId": 1, - "projectStatusId": 1, - "proposalReference": "1233", - "operatorId": 1 - }'::jsonb - where project_revision_id=3 - and form_data_table_name='project'; 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 new_form_data->>'projectName' from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project'), - 'Correct only newer', - 'The pending form change should have the value from the committing form change' -); - select is ( (select count(*) from cif.form_change where project_revision_id = 2 and form_data_table_name = 'project_attachment'), 0::bigint, @@ -59,12 +42,6 @@ select lives_ok ( 'Committing the pending project_revision does not throw an error' ); -select is ( - (select project_name from cif.project where id = 1), - 'Correct only newer', - 'The project table should have the updated proejct name, even after the pending amendment is committed' -); - select finish(); From 5c1c6208df4542a33a290a6c621bff25aa64096b Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 21 Mar 2024 22:13:06 -0700 Subject: [PATCH 46/51] chore: cleanup commit_form_change_internal --- .../mutations/commit_form_change_internal.sql | 111 ++++++++++-------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index a67dcea48c..c494c2c8d7 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -40,10 +40,10 @@ begin if pending_project_revision_id is not null then if fc.operation = 'create' then - /* - These are the forms that a project can have at most one of. - If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. - */ +/* + These are the forms that a project can have at most one of. + If pending has created one alredy, then we need to set the previous_form_change_id and the form_data_record_id. +*/ if ( (fc.json_schema_name in ('funding_parameter_EP', 'funding_parameter_IA', 'emission_intensity', 'project_summary_report')) and ((select count(id) from cif.form_change where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name) > 0) @@ -53,12 +53,20 @@ begin form_data_record_id = recordId where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name; + + +/* + If the committing form_change is creating a project contact, and the contactIndex already exists in the pending revision, + then update the index of the pending form_change to be the highest current index + 1 to avoid the clash. + If the contactIndex does not exist in the pending proejct revision, then the catch-all case for creates handles it. +*/ elsif ( fc.json_schema_name = 'project_contact' - and (select count(id) from cif.form_change where project_revision_id=pending_project_revision_id and json_schema_name=fc.json_schema_name) > 0 + and (select count(*) from cif.form_change where + project_revision_id = pending_project_revision_id and + json_schema_name = 'project_contact' and + new_form_data ->> 'contactIndex' = fc.new_form_data ->> 'contactIndex') > 0 ) then - -- if pending has any contacts, then create the new form change in pending and update the contactIndex to be the highest - -- contactIndex in pending + 1. If it doesn't then the catch-all for 'create' will handle it select id into new_fc_in_pending_id from cif.create_form_change( operation => 'update'::cif.form_change_operation, form_data_schema_name => 'cif', @@ -68,23 +76,23 @@ begin json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"contactIndex": %s}', (select max((new_form_data ->> 'contactIndex')::int) from cif.form_change - where project_revision_id=pending_project_revision_id + where project_revision_id = pending_project_revision_id and json_schema_name = fc.json_schema_name ) + 1)::jsonb ) ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; - /* - If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, - and the operation to 'update' to handle when pending has an opertion of 'create'. If not, then the catch all case will handle it. - */ +/* + If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, + and the operation to 'update' to handle when pending has an opertion of 'create'. If not, then the catch all case will handle it. +*/ elsif ( fc.json_schema_name = 'project_manager' and ( (select count(*) from cif.form_change - where project_revision_id=pending_project_revision_id - and json_schema_name=fc.json_schema_name + where project_revision_id = pending_project_revision_id + and json_schema_name = fc.json_schema_name and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId') > 0 ) ) then @@ -93,21 +101,21 @@ begin form_data_record_id = recordId, operation = 'update'::cif.form_change_operation where id = (select id from cif.form_change - where project_revision_id=pending_project_revision_id - and json_schema_name=fc.json_schema_name + where project_revision_id = pending_project_revision_id + and json_schema_name = fc.json_schema_name and new_form_data ->> 'projectManagerLabelId' = fc.new_form_data ->> 'projectManagerLabelId'); - /* - If reporting_requirements of this reportType already exist in pending, create the new form_change and set the reportingRequirementIndex - to the highest existing in pending plus 1. If committing is the first reporting_requirement of this reportType, then the catch-all works. - */ +/* + If reporting_requirements of this reportType already exist in pending, create the new form_change and set the reportingRequirementIndex + to the highest existing in pending plus 1. If committing is the first reporting_requirement of this reportType, then the catch-all works. +*/ elsif ( fc.json_schema_name = 'reporting_requirement' and ( - (select count(id) from cif.form_change where project_revision_id = pending_project_revision_id + (select count(*) 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') - > 0) + > 0) ) then select id into new_fc_in_pending_id from cif.create_form_change( operation => 'update'::cif.form_change_operation, @@ -126,11 +134,11 @@ begin ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; - /* - If committing is creating a milestone and milestones exist, create the milestone in pending and set the - reportingRequirementIndex to be the max existing in pending plus 1. - If pending has no milestones, the catch-all works. - */ +/* + If committing is creating a milestone and milestones exist, create the milestone in pending and set the + reportingRequirementIndex to be the max existing in pending plus 1. + If pending has no milestones, the catch-all works. +*/ elsif ( fc.json_schema_name = 'milestone' and ( @@ -154,12 +162,12 @@ begin ); update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; - /** - This next case acts as the catch-all for 'create'. It applies to any scenario in which the pending revision does not have an equivalent - form change to the one being committed. This includes unordered lists (e.g. attachments, milestones, etc.), the unique forms - like funding_parameter when pending has not created one yet, and ordered/pseudo ordered lists when the equivalent index does not - exist in pending. - **/ +/** + This next case acts as the catch-all for 'create'. It applies to any scenario in which the pending revision does not have an equivalent + form change to the one being committed. This includes unordered lists (e.g. attachments, milestones, etc.), the unique forms + like funding_parameter when pending has not created one yet, and ordered/pseudo ordered lists when the equivalent index does not + exist in pending. +**/ else select id into new_fc_in_pending_id from cif.create_form_change( @@ -188,11 +196,11 @@ begin select (cif.jsonb_minus(fc.new_form_data, parent_of_pending_form_change.new_form_data)) into committing_minus_pendings_parent; - /* - If the committing and pending form changes both have changes from the pending form change's parent, - then set the pending form change's new_form_data to be the committing form change's, and apply the changes - made in the pending form change to that data. - */ +/* + If the committing and pending form changes both have changes from the pending form change's parent, + then set the pending form change's new_form_data to be the committing form change's, and apply the changes + made in the pending form change to that data. +*/ if committing_minus_pendings_parent is not null then if pending_minus_pendings_parent is not null then update cif.form_change @@ -200,14 +208,13 @@ begin (fc.new_form_data || pending_minus_pendings_parent) where id = pending_form_change.id; - /* - If the pending form change hasn't made any changes since its creation, but the committing form change has, - set the pending form change's new_form_data to be the committing form_change's, as it is the latest information. - */ + /* + If the pending form change hasn't made any changes since its creation, but the committing form change has, + set the pending form change's new_form_data to be the committing form_change's, as it is the latest information. + */ else update cif.form_change - set new_form_data = - (fc.new_form_data) + set new_form_data = (fc.new_form_data) where id = pending_form_change.id; end if; end if; @@ -225,11 +232,11 @@ begin select (cif.jsonb_minus(pending_form_change.new_form_data, parent_of_pending_form_change.new_form_data)) into pending_minus_pendings_parent; - /* - If pending has made changes, then set its operation to create and null the form data record id & previous form change id - since it's technically creating them now. This way the archiving still took place in the committing form change, and we - avoid trying to update the now archived record that form_data_record_id points to. - */ +/* + If pending has made changes, then set its operation to create and null the form data record id & previous form change id + since it's technically creating them now. This way the archiving still took place in the committing form change, and we + avoid trying to update the now archived record that form_data_record_id points to. +*/ if pending_minus_pendings_parent is not null then update cif.form_change set operation = 'create'::cif.form_change_operation, @@ -238,9 +245,9 @@ begin where id = pending_form_change.id; else - /* - If pending has not made changes to the form data, the pending record can be deleted as it never would have been made - */ +/* + If pending has not made changes to the form data, the pending record can be deleted as it never would have been made +*/ delete from cif.form_change where project_revision_id = pending_project_revision_id and form_data_table_name = fc.form_data_table_name From 3a592c85f6bd5c4f3cb516b5261436f5feed8aa1 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Fri, 22 Mar 2024 22:48:44 -0700 Subject: [PATCH 47/51] chore: fix contact index clashes from happening when both committing and pending form changes create the same contact index --- .../mutations/commit_form_change_internal.sql | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/schema/deploy/mutations/commit_form_change_internal.sql b/schema/deploy/mutations/commit_form_change_internal.sql index c494c2c8d7..1a233cfdeb 100644 --- a/schema/deploy/mutations/commit_form_change_internal.sql +++ b/schema/deploy/mutations/commit_form_change_internal.sql @@ -56,10 +56,13 @@ begin /* - If the committing form_change is creating a project contact, and the contactIndex already exists in the pending revision, - then update the index of the pending form_change to be the highest current index + 1 to avoid the clash. - If the contactIndex does not exist in the pending proejct revision, then the catch-all case for creates handles it. + If the committing form_change is creating a project contact, and the contactIndex already exists in the pending revision, then we want + the contactId in pending to remain at that contactIndex. To do this, we create a new form_change in the pending project revision with + the contactId being commit, set its operation to create, and its contactIndex to one higher than the current highest index. We then + update the pending form change to have an operation of update, and give it the same form_data_record_id as committing, and assign the + committing id to be the previous_form_change_id. */ + elsif ( fc.json_schema_name = 'project_contact' and (select count(*) from cif.form_change where @@ -68,20 +71,26 @@ begin new_form_data ->> 'contactIndex' = fc.new_form_data ->> 'contactIndex') > 0 ) then select id into new_fc_in_pending_id from cif.create_form_change( - operation => 'update'::cif.form_change_operation, + operation => 'create'::cif.form_change_operation, form_data_schema_name => 'cif', form_data_table_name => fc.form_data_table_name, - form_data_record_id => recordId, + form_data_record_id => null, project_revision_id => pending_project_revision_id, json_schema_name => fc.json_schema_name, new_form_data => (fc.new_form_data || format('{"contactIndex": %s}', - (select max((new_form_data ->> 'contactIndex')::int) from cif.form_change + ((select max((new_form_data ->> 'contactIndex')::int) from cif.form_change where project_revision_id = pending_project_revision_id - and json_schema_name = fc.json_schema_name - ) + 1)::jsonb + and json_schema_name = 'project_contact' + ) + 1))::jsonb ) ); - update cif.form_change set previous_form_change_id = fc.id where id = new_fc_in_pending_id; + update cif.form_change set + operation = 'update'::cif.form_change_operation, + form_data_record_id = recordId, + previous_form_change_id = fc.id + where project_revision_id = pending_project_revision_id + and json_schema_name = 'project_contact' + and new_form_data ->> 'contactIndex' = fc.new_form_data ->> 'contactIndex'; /* If the projectManagerLabelId already exists in pending, set the form_data_record_id and previous_form_change_id, From 94b114227ad4c9d5921473befd13bfa557e94d2e Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Fri, 22 Mar 2024 22:51:48 -0700 Subject: [PATCH 48/51] test: fix contact scenarios in concurrent revision test --- .../conflicting_creates_test.sql | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql index ee5e38293b..665012c9e7 100644 --- a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql +++ b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql @@ -1,6 +1,6 @@ begin; -select plan(14); +select plan(15); truncate table cif.project, cif.operator, cif.contact, cif.attachment restart identity cascade; @@ -85,7 +85,6 @@ select cif.add_project_attachment_to_revision(2,1); -- create the general revision that will be "committing" select cif.create_project_revision(1, 'General Revision'); -- id = 3 --- Add necessary form changes for tests select cif.add_contact_to_revision(3, 1, 2); select cif.create_form_change( 'create', @@ -175,7 +174,6 @@ select cif.create_form_change( null, 3 ); -select cif.add_project_attachment_to_revision(3,1); select cif.add_project_attachment_to_revision(3,2); select cif.commit_project_revision(3); @@ -189,15 +187,15 @@ select is ( ); select is ( - (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '2'), - 2, - 'When committing and pending both create a primary contact, the committing primary becomes the amendments secondary contact' + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '1'), + 1, + 'When committing and pending both create a primary contact, the pending primary contact contactId maintains its value' ); select is ( - (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '1'), - 1, - 'When committing and pending both create a primary contact, the pending primary contact contactId maintains its value after commiting is commit' + (select (new_form_data->>'contactId')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact' and new_form_data->>'contactIndex' = '2'), + 2, + 'When committing and pending both create a primary contact, the committing primary becomes the amendments secondary contact' ); -- project_manager @@ -276,6 +274,12 @@ select lives_ok ( '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; From 59d154dec353f15c1e4458b1dd7601c4453367f2 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Sat, 23 Mar 2024 13:22:34 -0700 Subject: [PATCH 49/51] test: add emission_intensity and funding_parameter tests --- .../conflicting_creates_test.sql | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql index 665012c9e7..778d42ab09 100644 --- a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql +++ b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql @@ -1,6 +1,6 @@ begin; -select plan(15); +select plan(19); truncate table cif.project, cif.operator, cif.contact, cif.attachment restart identity cascade; @@ -31,6 +31,20 @@ select cif.commit_project_revision(1); -- create the amendment that will be "pending" select cif.create_project_revision(1, 'Amendment'); -- id = 2 select cif.add_contact_to_revision(2, 1, 1); +select cif.create_form_change( + 'create', + 'emission_intensity', + 'cif', + 'reporting_requirement', + json_build_object( + 'baselineEmissionIntensity', 1, + 'targetEmissionIntensity', 2, + 'postProjectEmissionIntensity', 3, + 'totalLifetimeEmissionReduction', 4 + )::jsonb, + null, + 2 +); select cif.create_form_change( 'create', 'funding_parameter_EP', @@ -86,6 +100,20 @@ select cif.add_project_attachment_to_revision(2,1); -- create the general revision that will be "committing" select cif.create_project_revision(1, 'General Revision'); -- id = 3 select cif.add_contact_to_revision(3, 1, 2); +select cif.create_form_change( + 'create', + 'emission_intensity', + 'cif', + 'reporting_requirement', + json_build_object( + 'baselineEmissionIntensity', 5, + 'targetEmissionIntensity', 6, + 'postProjectEmissionIntensity', 7, + 'totalLifetimeEmissionReduction', 8 + )::jsonb, + null, + 3 +); select cif.create_form_change( 'create', 'funding_parameter_EP', @@ -178,8 +206,34 @@ select cif.add_project_attachment_to_revision(3,2); select cif.commit_project_revision(3); --- emission_intensity --- project_contact +/* + emission_intensity + project_summary_report +*/ +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'funding_parameter_EP'), + 1::bigint, + 'When committing and pending have both created a funding parameter form, only one exists in pending after the first is commit' +); + +select is ( + (select (new_form_data ->> 'provinceSharePercentage')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'funding_parameter_EP'), + 1, + 'When committing and pending have both created a funding parameter form, pending maintains its form values' +); + +select is ( + (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'emission_intensity'), + 1::bigint, + 'When committing and pending have both created an emission intensity report form, only one exists in pending after the first is commit' +); + +select is ( + (select (new_form_data ->> 'baselineEmissionIntensity')::int from cif.form_change where project_revision_id = 2 and json_schema_name = 'emission_intensity'), + 1, + 'When committing and pending have both created an emission intensity report, pending maintains its form values' +); + select is ( (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'project_contact'), 2::bigint, From f15e53314cf7ecd805ebbe38aaf9e4e8cfb004cf Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Sat, 23 Mar 2024 13:27:31 -0700 Subject: [PATCH 50/51] test: confirm form_data_record_ids are being set in concurrent creates test --- .../concurrent_revisions/conflicting_creates_test.sql | 4 ---- .../concurrent_revisions/creating_new_records_test.sql | 8 +++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql index 778d42ab09..802e75a349 100644 --- a/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql +++ b/schema/test/unit/concurrent_revisions/conflicting_creates_test.sql @@ -206,10 +206,6 @@ select cif.add_project_attachment_to_revision(3,2); select cif.commit_project_revision(3); -/* - emission_intensity - project_summary_report -*/ select is ( (select count(*) from cif.form_change where project_revision_id = 2 and json_schema_name = 'funding_parameter_EP'), 1::bigint, diff --git a/schema/test/unit/concurrent_revisions/creating_new_records_test.sql b/schema/test/unit/concurrent_revisions/creating_new_records_test.sql index a23ebf5a55..0f64d69972 100644 --- a/schema/test/unit/concurrent_revisions/creating_new_records_test.sql +++ b/schema/test/unit/concurrent_revisions/creating_new_records_test.sql @@ -1,6 +1,6 @@ begin; -select plan(8); +select plan(9); /* Test when the committing project_revision creates records that do not exist in the pending project_revision @@ -114,6 +114,12 @@ select lives_ok ( '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; From 1a2ac241d87627021a39dab8a40892a3e22d8d69 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Sat, 23 Mar 2024 17:53:30 -0700 Subject: [PATCH 51/51] test: add tests for concurrent revisions that discard form changes chore: run precommit on all files --- docs/concurrentRevisionHandling.md | 3 +- .../mutations/commit_form_change_internal.sql | 4 +- .../discarding_attachment_test.sql | 49 ------ .../concurrent_revisions/discards_test.sql | 165 ++++++++++++++++++ 4 files changed, 169 insertions(+), 52 deletions(-) delete mode 100644 schema/test/unit/concurrent_revisions/discarding_attachment_test.sql create mode 100644 schema/test/unit/concurrent_revisions/discards_test.sql 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;