Skip to content

[DT-4061] Promote study registration asset lists to first-class fields - #3055

Open
otchet-broad wants to merge 1 commit into
otchet-dt-4061-study-recommendationsfrom
otchet-dt-4061-study-asset-fields
Open

[DT-4061] Promote study registration asset lists to first-class fields#3055
otchet-broad wants to merge 1 commit into
otchet-dt-4061-study-recommendationsfrom
otchet-dt-4061-study-asset-fields

Conversation

@otchet-broad

Copy link
Copy Markdown
Contributor

Base: branch 5 · 20 files, +1227/-16 · Migration: yes (2 changesets, data rewrite) · UI impact: designed to be none — verify

The riskiest PR in the stack, and the only one that rewrites existing data.

DatasetRegistrationSchemaV1.yaml declared assets as client-managed
metadata "preserved and returned as-is by the backend; not
backend-validated". Backend endpoints and dashboard counts came to depend on
individual keys inside it, so that contract no longer held. This promotes the
eight lists the backend reads — models, workspaces, presentations,
publications, clinicalTrials, intellectualProperties, biospecimens,
funding — to top-level registration fields, each stored in its own
study_property row, with schemas for the item types.

Compatibility is the point of the StudyAssets helper. Reads fall back to
the legacy object for a study not yet rewritten, and writes are still
accepted through it, so a client that has not moved to the top-level fields
keeps working. Registration reads and the search index both still return the
full assets object — every promoted list plus whatever unpromoted keys
remain — so no consumer sees a shape change.

Two details deserve review attention. A promoted property that parses is
authoritative even when empty, so a submitter who removed the last asset of a
type does not get it restored from a stale legacy copy. And "provided" means
present, not non-empty: registration reads return each list both top-level
and inside assets, so an edit clearing the top-level list arrives beside
the pre-edit legacy copy, and treating [] as "not provided" would resurrect
what was just removed.

Migration promotes each array-valued key out of assets into its own row,
then strips the promoted keys and drops any assets row left empty. Guarded
to skip non-object and non-array values and to skip a study that already has
the target row.

Rollback hazard: once the strip changeset runs, code from before this
branch can no longer see these lists — they no longer live in assets.
Rolling back the deploy requires restoring the data.

Also corrects IntellectualProperty.filingDate from boolean to string.

Depends on: nothing in branches 1-5 — no code dependency on any of them.
Placed last deliberately, because it is the only data rewrite in the stack and
the hardest to roll back. It can be reordered earlier if that suits review.


Where this sits in the DT-4061 stack

otchet-dt-3990-study-ratings-pi-details was too large to review meaningfully
(95 files, +8168), so it was split into eight PRs. This PR targets otchet-dt-4061-study-recommendations,
not develop
, so its diff shows only its own work.

# Branch Base
1 otchet-dt-4061-study-visibility-authz develop
1b otchet-dt-4061-study-patch-ownership otchet-dt-4061-study-visibility-authz
2 otchet-dt-4061-study-pi-details otchet-dt-4061-study-visibility-authz
3 otchet-dt-4061-study-comments otchet-dt-4061-study-pi-details
4 otchet-dt-4061-study-dar-metrics otchet-dt-4061-study-comments
5 otchet-dt-4061-study-recommendations otchet-dt-4061-study-dar-metrics
6 otchet-dt-4061-study-asset-fieldsthis PR otchet-dt-4061-study-recommendations
7 otchet-dt-4061-study-asset-endpoints otchet-dt-4061-study-asset-fields

Branch 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-*.sh helper scripts that were
dropped at the author's request. ./mvnw test-compile passes on each branch
independently.

🤖 Generated with Claude Code

@otchet-broad
otchet-broad marked this pull request as ready for review September 8, 2026 20:52
@otchet-broad
otchet-broad requested a review from a team as a code owner September 8, 2026 20:52
@otchet-broad
otchet-broad requested review from kevinmarete and rushtong and removed request for a team September 8, 2026 20:52
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from 37de009 to f7668a1 Compare September 8, 2026 21:58
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from d169eed to cd0fa78 Compare September 8, 2026 21:58

@kevinmarete kevinmarete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The promotion skips a key when its value is not an array, but the strip changeset removes that key regardless. For example, an existing unvalidated assets value containing "models": "custom-value" will lose that data. Please retain wrong-shaped values unless a value was successfully promoted or an authoritative first-class property already exists, and add a migration test for a JSON object containing a promoted-name key with a non-array value.

@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from f7668a1 to a7f8085 Compare September 9, 2026 13:05
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from cd0fa78 to 6a3d672 Compare September 9, 2026 13:05
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from a7f8085 to d41a878 Compare September 9, 2026 14:04
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from 6a3d672 to 223f8ef Compare September 9, 2026 14:04
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from d41a878 to a1c1f9e Compare September 9, 2026 14:33
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from 223f8ef to 415c6ee Compare September 9, 2026 14:33

@kevinmarete kevinmarete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a finding:

The runtime compatibility handling can discard legacy values that the migration deliberately preserves.

For example, the migration retains an unvalidated value such as "models": "custom-value" because it cannot promote it to an array. However, StudyAssets.assemble() removes the legacy models key and restores it only when it resolves to a nonempty list, so the value disappears from registration and search responses. On the next registration write, promotedValue() ignores the non-collection value while stripPromoted() removes it, deleting the only stored copy.

Please preserve wrong-shaped promoted-name values through both reads and writes, or explicitly migrate/reject them without silently losing data. Add a round-trip test covering a legacy assets object with a non-array value under a promoted key.

@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from a1c1f9e to 4d19126 Compare September 10, 2026 00:05
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from 415c6ee to 4c1dcff Compare September 10, 2026 00:05
DatasetRegistrationSchemaV1.yaml declared `assets` as client-managed
metadata "preserved and returned as-is by the backend; not
backend-validated". Backend endpoints and dashboard counts came to depend
on individual keys inside it, so that contract no longer held. This
promotes the eight lists the backend reads - models, workspaces,
presentations, publications, clinicalTrials, intellectualProperties,
biospecimens, funding - to top-level registration fields, each stored in
its own study_property row, with schemas for the item types.

Compatibility is the point of the StudyAssets helper. Reads fall back to
the legacy object for a study not yet rewritten, and writes are still
accepted through it, so a client that has not moved to the top-level
fields keeps working. Registration reads and the search index both still
return the full assets object - every promoted list plus whatever
unpromoted keys remain - so no consumer sees a shape change.

Two details worth review attention. A promoted property that parses is
authoritative even when empty, so a submitter who removed the last asset
of a type does not get it restored from a stale legacy copy. And
"provided" means present, not non-empty: registration reads return each
list both top-level and inside `assets`, so an edit clearing the
top-level list arrives beside the pre-edit legacy copy, and treating []
as "not provided" would resurrect what was just removed.

Migration promotes each array-valued key out of `assets` into its own
row, then strips a key only once its value is safely stored elsewhere -
either this migration promoted it, or a first-class property already
existed. A key whose value is the wrong shape was never promoted, so it
stays put: `assets` was unvalidated and free to hold anything, and
dropping such a value would lose the only copy of it. An empty list is
likewise not promoted and so is retained, which nothing reads either way.
Any `assets` row left empty is deleted.

The promotion compares against the empty array rather than measuring with
jsonb_array_length. The planner may evaluate that before the typeof
guard, and the length function raises "cannot get array length of a
scalar" on a string or number value - which the migration test now covers
with a real one.

Note for reviewers who ran the earlier version of this changeset against
a local database: the checksum changed, so Liquibase will need a clear or
a fresh database. The changeset ids are unchanged.

Also corrects IntellectualProperty.filingDate from boolean to string.

The runtime path now preserves what the migration preserves. The
migration keeps a legacy value under a promoted name that is not a list,
because the promotion cannot take it - but assemble() removed the legacy
key and restored it only when it resolved to a nonempty list, so the
value disappeared from registration and search reads, and stripPromoted()
then removed it on the next write while promotedValue() ignored it,
deleting the only stored copy. assemble() now leaves such a value exactly
as stored, and stripPromoted() removes a promoted key only when its value
is a list, which is the same shape test promotedValue() applies. A
round-trip test covers a legacy assets object with a non-array value
under a promoted key, and fails against the previous behavior.

The one combination still not representable is a study holding both a
promoted property and a non-list legacy value under the same name: one
key cannot carry both. The promoted list wins, being the shape clients
expect and the newer intent, and the legacy value is left on disk rather
than stripped. The migration does not produce that combination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-recommendations branch from 4d19126 to 7f7d63e Compare September 10, 2026 00:40
@otchet-broad
otchet-broad force-pushed the otchet-dt-4061-study-asset-fields branch from 4c1dcff to 945bae9 Compare September 10, 2026 00:40

@kevinmarete kevinmarete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding:

promotedValue() performs a case-sensitive lookup with assets.get(key), while stripPromoted() removes list-valued keys using equalsIgnoreCase. Consequently, a legacy value such as "Models": [...] is not promoted but is removed from assets, losing the data during a registration update. Please use the same case-insensitive lookup in both paths and add a round-trip test for a differently cased promoted key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants