[DT-4061] Add PI institution, ORCID, and profile links to studies - #3051
[DT-4061] Add PI institution, ORCID, and profile links to studies#3051otchet-broad wants to merge 1 commit into
Conversation
8f0be37 to
dc1f70e
Compare
1f57fc7 to
0233a80
Compare
kevinmarete
left a comment
There was a problem hiding this comment.
Here are some findings:
-
P1 — PI institution can be silently reverted.
PATCH updates the new column, but the registration response builder still reads the legacy study property at SchemaFromStudy.java#L89.Example: institution
1is backfilled, PATCH changes it to2, registration GET still returns1, and a subsequent registration PUT writes1back into the column. The builder should treatstudy.piInstitutionas authoritative, with a PATCH → registration GET → PUT regression test. -
P2 — Study responses fabricate an institution creation date.
StudyReducer.java#L26 constructs a partialInstitution, whose constructor initializescreateDateto the current time at Institution.java#L44. Every study read therefore returns a false, changingpiInstitution.createDate. The query should map the actual institution metadata, or the API should use a dedicated ID/name summary model.
72d3139 to
ab1833b
Compare
0233a80 to
68ba657
Compare
Adds pi_institution_id, pi_orcid, pi_linkedin_url and pi_website_url to the study table and surfaces them on Study, StudyPatch, and the study registration read path. Registration already collected the PI institution as the numeric `piInstitution` study property, so createStudy now records the column too, and a backfill changeset copies the property into the column for studies registered earlier. Without the backfill every existing study shows a blank PI Institution until someone re-PATCHes it. The backfill skips values that are not plain integers and ids with no matching institution row, which the foreign key would otherwise reject. The column is authoritative wherever the two disagree. SchemaFromStudy builds the registration payload from it, falling back to the legacy property only when the column is null (which the backfill leaves it when the recorded id matched no institution). Reading the property there instead would silently revert a PATCH: a registration GET would report the pre-PATCH institution and the next registration PUT would write it back. Covered by testPatchedPiInstitutionSurvivesARegistrationRoundTrip. Review focus: these are columns, not study properties, so they deliberately do not follow the "blank string deletes" convention the patchable properties use. They follow the JSON convention instead - absent is a no-op, explicit null clears, blank normalizes to a clear. Jackson cannot tell an absent field from an explicit null, so StudyPatch.fromJson re-reads the body to record which field names arrived as literal nulls. A registration PUT carries the PI institution but not the profile links, which are PATCH-only, so DatasetRegistrationService carries the stored links forward rather than dropping them; a study conversion does the same. executeUpdateStudy now raises NotFoundException for a missing study instead of dereferencing null. The study queries select the PI institution's real create and update dates, and StudyReducer always overwrites them - with the stored value where the join supplied it, null where it did not. The Institution constructor seeds createDate with "now", which would otherwise surface on every study read as a fabricated timestamp that changes each time. Additive on the wire: four new fields on study reads, accepted on PATCH, no existing field changes shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ab1833b to
63e5dd3
Compare
68ba657 to
3d192ec
Compare
fboulnois
left a comment
There was a problem hiding this comment.
Some issues I identified with Claude and Codex:
1. An out-of-range value aborts the backfill
I ran the shipped SQL against postgres:16.10-alpine with piInstitution = 99999999999. It passes ^[0-9]+$, then fails the integer cast:
ERROR: value "99999999999" is out of range for type integer
This fails the entire changeset, including the backfill of valid rows. Limiting the digit count avoids the error:
AND btrim(sp.value) ~ '^[0-9]{1,9}$'I confirmed this backfills the valid row and skips the out-of-range value. It also excludes valid 10-digit integer IDs, so a full range check would be needed to support those.
2. Both write paths need to validate piInstitutionId
PATCH: DatasetService.java:826-830 catches every exception and throws InternalServerErrorException, so {"piInstitutionId": 999999} returns 500 instead of 400 when the institution doesn't exist.
Registration: checkPiInstitutionRequired only checks for null. Previously, a nonexistent ID was stored as a text property and registration succeeded. Now the foreign key constraint fails the entire registration transaction. This changes behavior for existing clients and needs explicit validation.
3. PATCH leaves conflicting institution values in study responses
After a PATCH, the study response contains the new piInstitution.id and the old properties[piInstitution] value. SchemaFromStudy handles this for registration payloads, but raw study responses still expose both. Update the legacy property in patchStudy or stop returning it.
4. The backfill test doesn't cover the EXISTS guard
testSkipsValuesThatCannotBeAppliedToTheColumn uses -1 as an orphan ID, but that fails the regex before reaching EXISTS. Use a positive ID with no institution row, such as 2000000000, and add a separate out-of-range case for item 1.
5. The timestamp fix needs a test
The commit fixes the constructor's default createDate by always overwriting it in the reducer, but no test asserts getPiInstitution().getCreateDate().
Since hasOptionalColumn catches every exception and returns empty, a broken alias or mapping could silently produce null dates. Test both populated and null timestamps.
6. Registration updates fetch the study twice
updateStudyFromRegistration calls findStudyById just to read three strings. That opens a REPEATABLE_READ transaction and loads properties, dataset IDs and files. executeUpdateStudy then fetches the study again in the write transaction.
Read the links inside executeUpdateStudy, where the study is already loaded, to avoid the extra fetch and the gap between reading and writing.
7. Smaller issues
- The 18-argument constructor and 13-argument
updateStudyare easy to misuse, especially with several adjacentStringarguments and all-null call sites. A builder or test fixture would help, as would assertions more specific thanany()for every argument. piInstitutionIdaccepts"123"as an integer, unlike the DTO's strict string and boolean deserializers. Add equivalent integer validation for consistency.- Add a test showing that
{"piName": null}remains a no-op while{"piOrcid": null}clears the field. Dataset.yamlreusesStudy.yamland documentspiInstitution, butDatasetDAO.findDatasetStudyByIddoesn't selectpi_institution_id, so the field is always null on that route.
I also checked that stringPatchProps() excludes the PI columns: a blank piOrcid correctly avoids deleting a study property of that name.
Base: branch 1 · 20 files, +1165/-22 · Migration: yes (2 changesets) · UI impact: additive only
Adds
pi_institution_id,pi_orcid,pi_linkedin_url,pi_website_urltothe
studytable and surfaces them onStudy,StudyPatch, and the studyregistration read path.
Registration already collected the PI institution as the numeric
piInstitutionstudy property, socreateStudynow records the column too,and a backfill changeset copies the property into the column for studies
registered earlier. Without the backfill every existing study shows a blank
PI Institution until someone re-PATCHes it. The backfill skips values that
are not plain integers and ids with no matching institution row, which the
foreign key would otherwise reject.
Review focus: these are columns, not study properties, so they deliberately
do not follow the "blank string deletes" convention the patchable
properties use. They follow the JSON convention instead — absent is a no-op,
explicit
nullclears, blank normalizes to a clear. Jackson cannot tell anabsent field from an explicit null, so
StudyPatch.fromJsonre-reads thebody to record which field names arrived as literal nulls.
A registration PUT carries the PI institution but not the profile links,
which are PATCH-only, so
DatasetRegistrationServicecarries the storedlinks forward rather than dropping them; a study conversion does the same.
executeUpdateStudynow raisesNotFoundExceptionfor a missing studyinstead of dereferencing null.
Additive on the wire: four new fields on study reads, accepted on PATCH, no
existing field changes shape.
Depends on: branch 1 — shares hunks in
DatasetService.javaandDatasetServiceTest. That file overlap is the only reason it is stacked here.Where this sits in the DT-4061 stack
otchet-dt-3990-study-ratings-pi-detailswas too large to review meaningfully(95 files, +8168), so it was split into eight PRs. This PR targets
otchet-dt-4061-study-visibility-authz,not
develop, so its diff shows only its own work.otchet-dt-4061-study-visibility-authzdevelopotchet-dt-4061-study-patch-ownershipotchet-dt-4061-study-visibility-authzotchet-dt-4061-study-pi-details← this PRotchet-dt-4061-study-visibility-authzotchet-dt-4061-study-commentsotchet-dt-4061-study-pi-detailsotchet-dt-4061-study-dar-metricsotchet-dt-4061-study-commentsotchet-dt-4061-study-recommendationsotchet-dt-4061-study-dar-metricsotchet-dt-4061-study-asset-fieldsotchet-dt-4061-study-recommendationsotchet-dt-4061-study-asset-endpointsotchet-dt-4061-study-asset-fieldsBranch 1b is a sibling rather than a link in the chain: nothing depends on it,
so it can be held or dropped without blocking the others. Land the numbered
chain in order — merging out of order, or squash-merging, will require rebasing
the descendants.
The split is verified lossless: branch 7 plus 1b reproduces the original branch
exactly, apart from five
scripts/verify-study-*.shhelper scripts that weredropped at the author's request.
./mvnw test-compilepasses on each branchindependently.
🤖 Generated with Claude Code